packages feed

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 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