ghcitui 0.2.0.0 → 0.3.0.0
raw patch · 11 files changed
+193/−76 lines, 11 filesdep ~textPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: text
API changes (from Hackage documentation)
Files
- CHANGELOG.md +22/−0
- ghcitui.cabal +3/−1
- lib/ghcitui-brick/Ghcitui/Brick/AppConfig.hs +1/−0
- lib/ghcitui-brick/Ghcitui/Brick/AppInterpState.hs +1/−0
- lib/ghcitui-brick/Ghcitui/Brick/AppState.hs +21/−6
- lib/ghcitui-brick/Ghcitui/Brick/AppTopLevel.hs +2/−1
- lib/ghcitui-brick/Ghcitui/Brick/Events.hs +8/−3
- lib/ghcitui-brick/Ghcitui/Brick/SourceWindow.hs +69/−54
- lib/ghcitui-core/Ghcitui/Ghcid/ParseContext.hs +11/−11
- test/ParseContextSpec.hs +53/−0
- test/Spec.hs +2/−0
CHANGELOG.md view
@@ -1,5 +1,23 @@ # Revision history for ghcitui +## 0.3.0.0 -- 2024-03-17++### API Changes++- **Ghcitui.Brick**+ - Large rework of SourceWindow's end calculation.+ - Removed `updateSrcWindowEnd`, replaced with `updateVerticalSpace`.+ - Added `srcWindowLineDiffCount`.++### Bug fixes++- Can now parse functions with apostraphes in names. (Issue #38)+- Switching between files when updating contexts now snaps to the stopped line (Issue #41)++### Known issues++See https://github.com/CrystalSplitter/ghcitui/issues for the latest issues.+ ## 0.2.0.0 -- 2024-02-11 ### New Features@@ -33,6 +51,10 @@ ### Known issues See https://github.com/CrystalSplitter/ghcitui/issues for the latest issues.++- Can't parse functions with apostrophes in names. (Issue #38) (fixed in 0.3.0.0)+- Switching between files when updating contexts does not snap to the stopped line (Issue #41)+ (fixed in 0.3.0.0) ## 0.1.0.0 -- 2024-01-21
ghcitui.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: ghcitui-version: 0.2.0.0+version: 0.3.0.0 synopsis: A Terminal User Interface (TUI) for GHCi description:@@ -150,8 +150,10 @@ main-is: Spec.hs type: exitcode-stdio-1.0 build-depends: base >= 4.16 && < 5+ , text , ghcitui , hspec ^>= 2.11.5 other-modules: LocSpec+ , ParseContextSpec , UtilSpec default-language: Haskell2010
lib/ghcitui-brick/Ghcitui/Brick/AppConfig.hs view
@@ -44,6 +44,7 @@ , getStartupCommands :: ![T.Text] -- ^ Commands to run in ghci during start up. }+ deriving (Show) defaultConfig :: AppConfig defaultConfig =
lib/ghcitui-brick/Ghcitui/Brick/AppInterpState.hs view
@@ -33,6 +33,7 @@ , historyPos :: !Int -- ^ Current position }+ deriving (Show) -- | Lens accessor for the editor. See '_liveEditor'. liveEditor :: Lens.Lens' (AppInterpState s n) (BE.Editor s n)
lib/ghcitui-brick/Ghcitui/Brick/AppState.hs view
@@ -67,6 +67,7 @@ { _wsInfoWidth :: !Int , _wsReplHeight :: !Int }+ deriving (Show) {- | Application state wrapper. @@ -106,6 +107,7 @@ , splashContents :: !(Maybe T.Text) -- ^ Splash to show on start up. }+ deriving (Show) newtype AppStateM m a = AppStateM {runAppStateM :: m a} @@ -171,14 +173,27 @@ selectPausedLine :: (Ord n) => AppState n -> B.EventM n m (AppState n) selectPausedLine s@AppState{interpState} = do s' <- setSelectedFile ourSelectedFile s+ let ourSelectedLine :: Int+ ourSelectedLine =+ fromMaybe+ (selectedLine s')+ (Loc.startLine . Loc.fSourceRange =<< interpState.pauseLoc) newSrcW <- SourceWindow.setSelectionTo ourSelectedLine (s' ^. sourceWindow)- pure $ Lens.set sourceWindow newSrcW s'+ pure+ . ( \s'' ->+ writeDebugLog+ ( "replacing source window. new line: "+ <> Util.showT (s'' ^. sourceWindow . SourceWindow.srcSelectedLineL)+ <> " should be "+ <> Util.showT ourSelectedLine+ <> ", window start "+ <> Util.showT (s'' ^. sourceWindow . SourceWindow.srcWindowStartL)+ )+ s''+ )+ . Lens.set sourceWindow newSrcW+ $ s' where- ourSelectedLine :: Int- ourSelectedLine =- fromMaybe- (selectedLine s)- (Loc.startLine . Loc.fSourceRange =<< interpState.pauseLoc) ourSelectedFile = maybe (selectedFile s) (Just . Loc.filepath) interpState.pauseLoc -- | Write a debug log entry.
lib/ghcitui-brick/Ghcitui/Brick/AppTopLevel.hs view
@@ -12,5 +12,6 @@ | BindingViewport | ModulesViewport | TraceViewport- | SourceList+ | -- | Source Window Name.+ SourceList deriving (Eq, Show, Ord)
lib/ghcitui-brick/Ghcitui/Brick/Events.hs view
@@ -31,9 +31,11 @@ handleEvent (B.VtyEvent (V.EvResize _ _)) = B.invalidateCache handleEvent ev = do appState <- B.get- updatedSourceWindow <- SourceWindow.updateSrcWindowEnd (appState ^. AppState.sourceWindow)+ updatedSourceWindow <- SourceWindow.updateVerticalSpace (appState ^. AppState.sourceWindow) let appStateUpdated = Lens.set AppState.sourceWindow updatedSourceWindow appState- let handler = case appStateUpdated.activeWindow of+ B.put appStateUpdated+ let handler :: B.BrickEvent AppName e -> B.EventM AppName (AppState AppName) ()+ handler = case appStateUpdated.activeWindow of AppState.ActiveCodeViewport -> handleSrcWindowEvent AppState.ActiveLiveInterpreter -> handleInterpreterEvent AppState.ActiveInfoWindow -> handleInfoEvent@@ -243,7 +245,9 @@ -- TODO: Should be configurable? interpreterLogLimit = 1000 --- | Reflow entries of text into columns.+{- | Reflow entries of text into columns.+ Mostly useful right now for printing autocomplete suggestions into columns.+-} reflowText :: Int -- ^ Num columns@@ -263,6 +267,7 @@ maxTextLen = colWidth - 1 makeLine xs = T.concat (T.justifyLeft colWidth ' ' . shortenText maxTextLen <$> xs) +-- | Limit text to a given length, and cut with an elipses. shortenText :: Int -> T.Text -> T.Text shortenText maxLen text | len <= maxLen = text
lib/ghcitui-brick/Ghcitui/Brick/SourceWindow.hs view
@@ -16,16 +16,17 @@ , ScrollDir (..) , scrollTo , srcWindowScrollPage- , updateSrcWindowEnd , srcWindowMoveSelectionBy , srcWindowReplace , setSelectionTo+ , updateVerticalSpace -- * Lenses , srcElementsL , srcNameL , srcSelectedLineL , srcWindowStartL+ , srcWindowVerticalSpaceL -- * Misc , srcWindowLength@@ -48,7 +49,8 @@ , srcWindowStart :: !Int -- ^ The starting position of the window, as a line number (1-indexed). -- No lines before this line number is rendered.- , srcWindowEnd :: !(Maybe Int)+ , srcWindowVerticalSpace :: !(Maybe Int)+ -- ^ The maximum amount of visible lines at any point in time. , srcName :: !name -- ^ The name of the window. , srcSelectedLine :: !(Maybe Int)@@ -59,12 +61,23 @@ makeLensesFor [ ("srcElements", "srcElementsL") , ("srcWindowStart", "srcWindowStartL")- , ("srcWindowEnd", "srcWindowEndL")+ , ("srcWindowVerticalSpace", "srcWindowVerticalSpaceL") , ("srcName", "srcNameL") , ("srcSelectedLine", "srcSelectedLineL") ] ''SourceWindow +-- | The difference between the last rendered line and the first rendered line.+srcWindowLineDiffCount :: SourceWindow name elem -> Maybe Int+srcWindowLineDiffCount SourceWindow{srcWindowVerticalSpace = Just sWVS} = pure $ sWVS - 1+srcWindowLineDiffCount _ = Nothing++-- | The line number of the last viewable line in the window.+getLastRenderedLine :: SourceWindow name elem -> Maybe Int+getLastRenderedLine srcW@SourceWindow{srcWindowStart} = do+ diffCount <- srcWindowLineDiffCount srcW+ pure $ diffCount + srcWindowStart+ -- | Render a 'SourceWindow' into a Brick 'B.Widget'. renderSourceWindow :: (Ord n)@@ -103,23 +116,27 @@ srcWindowLength :: SourceWindow n e -> Int srcWindowLength = Vec.length . srcElements --- | Set the source window end line inside of the given 'EventM' Monad.-updateSrcWindowEnd :: (Ord n) => SourceWindow n e -> B.EventM n m (SourceWindow n e)-updateSrcWindowEnd srcW@SourceWindow{srcWindowStart, srcName} = do- mExtent <- B.lookupExtent srcName- let end = case mExtent of+{- | Set the source window end line inside of the given 'EventM' Monad.+ This is primarily for internal consistency, and is cheap. It should be called any time+ the srcWindowStart changes.+-}+updateVerticalSpace :: (Ord n) => SourceWindow n e -> B.EventM n m (SourceWindow n e)+updateVerticalSpace srcW@SourceWindow{srcName {- , srcContainerName -}} = do+ mSrcNameExtent <- B.lookupExtent srcName+ let mSpace = case mSrcNameExtent of Just extent ->- -- -1 offset since the end is inclusive.- Just $ (snd . B.extentSize $ extent) + srcWindowStart - 1+ Just . snd . B.extentSize $ extent _ -> Nothing- pure (Lens.set srcWindowEndL end srcW)+ pure (Lens.set srcWindowVerticalSpaceL mSpace srcW) -- | Scroll to a given position, and move the source line along the way if needed. scrollTo :: Int -> SourceWindow n e -> SourceWindow n e-scrollTo pos srcW@SourceWindow{srcWindowEnd = Just windowEnd} =+scrollTo pos srcW@SourceWindow{srcWindowVerticalSpace = Just vSpace} = srcW{srcWindowStart = clampedPos, srcSelectedLine = newSelection} where- clampedPos = Util.clamp (1, srcWindowLength srcW - renderHeight) pos+ -- Clamp between start line and one window away from the end.+ clampedPos = Util.clamp (1, srcWindowLength srcW - vSpace) pos+ newSelection | -- Choose the starting line if we're trying to go past the beginning. isScrollingPastStart =@@ -128,13 +145,13 @@ isScrollingPastEnd = Just $ srcWindowLength srcW | otherwise = newClampedSelectedLine- renderHeight = windowEnd - srcWindowStart srcW isScrollingPastStart = pos < 1 isScrollingPastEnd = pos >= srcWindowLength srcW -- Using >= because of a hack.- newClampedSelectedLine =- Util.clamp- (clampedPos, clampedPos + renderHeight)- <$> srcSelectedLine srcW+ newClampedSelectedLine :: Maybe Int+ newClampedSelectedLine = do+ ssl <- srcSelectedLine srcW+ diffCount <- srcWindowLineDiffCount srcW+ pure $ Util.clamp (clampedPos, clampedPos + diffCount) ssl scrollTo _ srcW = srcW -- | Direction to scroll by.@@ -142,18 +159,16 @@ -- | Scroll by a full page in a direction. srcWindowScrollPage :: (Ord n) => ScrollDir -> SourceWindow n e -> B.EventM n m (SourceWindow n e)-srcWindowScrollPage dir srcW = srcWindowScrollPage' dir <$> updateSrcWindowEnd srcW+srcWindowScrollPage dir srcW = srcWindowScrollPage' dir <$> updateVerticalSpace srcW --- | Internal helper. srcWindowScrollPage' :: ScrollDir -> SourceWindow n e -> SourceWindow n e-srcWindowScrollPage' dir srcW =+srcWindowScrollPage' dir srcW@SourceWindow{srcWindowStart} = case dir of- Up ->- let renderHeight = windowEnd - srcWindowStart srcW- in scrollTo (srcWindowStart srcW - renderHeight) srcW- Down -> scrollTo windowEnd srcW+ Up -> scrollTo onePageUpPos srcW+ Down -> scrollTo (fromMaybe srcWindowStart (getLastRenderedLine srcW)) srcW where- windowEnd = fromMaybe 1 $ srcWindowEnd srcW+ onePageUpPos = srcWindowStart - vSpace + 1 -- Plus one to preserve the top line.+ vSpace = fromMaybe 0 (srcWindowVerticalSpace srcW) -- | Set the selection to a given position, and scroll the window accordingly. setSelectionTo@@ -163,15 +178,21 @@ -> SourceWindow n e -- ^ Source window to update. -> B.EventM n m (SourceWindow n e)-setSelectionTo pos srcW@SourceWindow{srcSelectedLine = Just sl, srcWindowEnd = Just end} =- if pos < srcWindowStart srcW || pos > end- then srcWindowMoveSelectionBy delta srcW- else do- pure $ srcW{srcSelectedLine = Just pos}- where- delta = pos - sl-setSelectionTo _ srcW = pure srcW+setSelectionTo pos srcW = do+ srcW' <- updateVerticalSpace srcW+ case (getLastRenderedLine srcW', srcSelectedLine srcW') of+ (Just end, Just oldSelectedLine) -> do+ let delta = pos - oldSelectedLine+ if pos < srcWindowStart srcW' || pos > end+ then srcWindowMoveSelectionBy delta srcW+ else do+ pure $ srcW{srcSelectedLine = Just pos}+ _ -> setSelectionToFallback pos srcW' +-- | Fallback function for setting the source window selection line, when we can't set it properly.+setSelectionToFallback :: Int -> SourceWindow name elem -> B.EventM name m (SourceWindow name elem)+setSelectionToFallback pos srcW = pure $ srcW{srcSelectedLine = Just pos, srcWindowStart = pos}+ -- | Move the selected line by a given amount. srcWindowMoveSelectionBy :: (Ord n)@@ -181,23 +202,17 @@ -- ^ Source window to update. -> B.EventM n m (SourceWindow n e) srcWindowMoveSelectionBy amnt sw = do- srcW' <- updateSrcWindowEnd sw- case srcWindowEnd srcW' of- Just end -> do- let start = srcWindowStart srcW'- let mSLine = srcSelectedLine srcW'- let renderHeight = end - start- pure $ case mSLine of- Just sLine- | newSLine < start ->- scrollTo newSLine srcW'{srcSelectedLine = Just newSLine}- | newSLine > end ->- scrollTo (newSLine - renderHeight) srcW'{srcSelectedLine = Just newSLine}- | otherwise -> srcW'{srcSelectedLine = Just newSLine}- where- newSLine = Util.clamp (1, Vec.length (srcElements srcW')) $ sLine + amnt- _ -> srcW'- Nothing -> pure srcW'+ srcW <- updateVerticalSpace sw+ case (getLastRenderedLine srcW, srcWindowLineDiffCount srcW, srcSelectedLine srcW) of+ (Just end, Just renderHeight, Just oldSLine)+ | newSLine < srcWindowStart srcW ->+ pure $ scrollTo newSLine srcW{srcSelectedLine = Just newSLine}+ | newSLine > end ->+ pure $ scrollTo (newSLine - renderHeight) srcW{srcSelectedLine = Just newSLine}+ | otherwise -> pure $ srcW{srcSelectedLine = Just newSLine}+ where+ newSLine = Util.clamp (1, Vec.length (srcElements srcW)) $ oldSLine + amnt+ _ -> pure srcW {- | Replace the contents of a given source window, and reset the pseudo-viewport's position to the top.@@ -215,13 +230,13 @@ -> T.Text -- ^ Text contents of the source window (to be split up). -> SourceWindow n T.Text-mkSourcWindow name text =+mkSourcWindow sourceWindowName text = SourceWindow { srcElements = lineVec , srcWindowStart = 1 , srcSelectedLine = Just 1- , srcName = name- , srcWindowEnd = Nothing+ , srcName = sourceWindowName+ , srcWindowVerticalSpace = Nothing } where lineVec = Vec.fromList (T.lines text)
lib/ghcitui-core/Ghcitui/Ghcid/ParseContext.hs view
@@ -41,9 +41,9 @@ , filepath :: !FilePath , pcSourceRange :: !Loc.SourceRange }- deriving (Show)+ deriving (Eq, Show) -data ParseContextReturn = PCError ParseError | PCNoContext | PCContext ParseContextOut+data ParseContextReturn = PCError ParseError | PCNoContext | PCContext ParseContextOut deriving (Eq, Show) -- | Parse the output from ":show context" for the interpreter state. parseContext :: T.Text -> ParseContextReturn@@ -58,7 +58,7 @@ let contextTextLines = T.lines contextText in if all (`elem` ["", "()"]) contextTextLines then PCNoContext- else PCError (ParseError [i| parsing context: #{e}|])+ else PCError (ParseError [i|parsing context: #{e}|]) parseFile :: T.Text -> Either ParseError FilePath parseFile s@@ -68,9 +68,9 @@ -- | Parse a source range structure into a SourceRange object. parseSourceRange :: T.Text -> Loc.SourceRange parseSourceRange s- -- Matches (12,34)-(56,78)+ -- Matches (12,34)-(56,78) ... (line 12, column 34 to line 56, column 78) | Just mr <- matches "\\(([0-9]+),([0-9]+)\\)-\\(([0-9]+),([0-9]+)\\)" = fullRange mr- -- Matches 12:34-56+ -- Matches 12:34-56 ... (line 12, columns 34 to 56) | Just mr <- matches "([0-9]+):([0-9]+)-([0-9]+)" = lineColRange mr -- Matches 12:34 | Just mr <- matches "([0-9]+):([0-9]+)" = lineColSingle mr@@ -128,15 +128,15 @@ eInfoLine "" = Left $ ParseError "Could not find info line in empty string" eInfoLine contextText = note- (ParseError $ "Could not match info line: '" <> showT splits <> "'")- stopLine+ (ParseError [i|Could not match info line: '#{showT splits}'|])+ mStopLine where splits = splitBy ghcidPrompt contextText- stopLineMR = foldr (\n acc -> acc <|> stopReg n) Nothing splits- stopLine = (\mr -> (mrSubs mr ! 1, mrSubs mr ! 2)) <$> stopLineMR+ mStopLine = (\mr -> (mrSubs mr ! 1, mrSubs mr ! 2)) <$> mStopLineMatchRes+ mStopLineMatchRes = foldr (\n acc -> acc <|> stopReg n) Nothing splits -- Match on the "Stopped in ..." line. stopReg :: T.Text -> Maybe (MatchResult T.Text)- stopReg s = s =~~ ("^[ \t]*Stopped in ([[:alnum:]_.()]+),(.*)" :: T.Text)+ stopReg s = s =~~ ("^[ \t]*Stopped in ([[:alnum:]_.()']+),(.*)" :: T.Text) parseBreakResponse :: T.Text -> Either T.Text [Loc.ModuleLoc] parseBreakResponse t@@ -212,7 +212,7 @@ where stripped = T.strip t matchingLines = mapMaybe matching . T.lines <$> lastMay (splitBy ghcidPrompt stripped)- reg = "([[:alnum:]_.]+)[ \\t]+\\( *([^,]*),.*\\)" :: T.Text+ reg = "([[:alnum:]_.']+)[ \\t]+\\( *([^,]*),.*\\)" :: T.Text matching :: T.Text -> Maybe (MatchResult T.Text) matching = (=~~ reg)
+ test/ParseContextSpec.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}++module ParseContextSpec where++import Data.Text as T+import Test.Hspec++import qualified Ghcitui.Ghcid.ParseContext as PC+import qualified Ghcitui.Loc as Loc++spec :: Spec+spec = do+ describe "parseContext" $ do+ it "can parse fibonacci context" $ do+ let expectedLoc =+ Loc.SourceRange+ { Loc.startLine = Just 9+ , Loc.startCol = Just 17+ , Loc.endLine = Just 9+ , Loc.endCol = Just 29+ }+ let expected =+ PC.PCContext (PC.ParseContextOut "Yib.fibty.right" "test/Fib.hs" expectedLoc)+ PC.parseContext fibFixture `shouldBe` expected+ it "can parse the Ormolu function parseModule' (with an apostraphe)" $ do+ let expectedLoc =+ Loc.SourceRange+ { Loc.startLine = Just 257+ , Loc.startCol = Just 51+ , Loc.endLine = Just 261+ , Loc.endCol = Just 35+ }+ let expected =+ PC.PCContext+ (PC.ParseContextOut "Ormolu.parseModule'" "src/Ormolu.hs" expectedLoc)+ PC.parseContext apostrapheFixture `shouldBe` expected+++fibFixture :: T.Text+fibFixture =+ T.unlines+ [ "[test/Fib.hs:9:17-29] #~GHCID-START~#[test/Fib.hs:9:17-29] #~GHCID-START~#--> fibty 10"+ , " Stopped in Yib.fibty.right, test/Fib.hs:9:17-29"+ ]++apostrapheFixture :: T.Text+apostrapheFixture =+ T.unlines+ [ "()"+ , "[src/Ormolu.hs:(257,51)-(261,35)] #~GHCID-START~#()"+ , "[src/Ormolu.hs:(257,51)-(261,35)] #~GHCID-START~#--> invoke"+ , " Stopped in Ormolu.parseModule', src/Ormolu.hs:(257,51)-(261,35)"+ ]
test/Spec.hs view
@@ -3,9 +3,11 @@ import Test.Hspec import qualified LocSpec+import qualified ParseContextSpec import qualified UtilSpec main :: IO () main = hspec $ do LocSpec.spec UtilSpec.spec+ ParseContextSpec.spec