diff --git a/src/Swarm/TUI/Controller.hs b/src/Swarm/TUI/Controller.hs index 536c50b48..6ddf512d1 100644 --- a/src/Swarm/TUI/Controller.hs +++ b/src/Swarm/TUI/Controller.hs @@ -815,6 +815,8 @@ updateUI = do let itName = fromString $ "it" ++ show itIx let out = T.intercalate " " [itName, ":", prettyText finalType, "=", into (prettyValue v)] uiState . uiREPL . replHistory %= addREPLItem (REPLOutput out) + invalidateCacheEntry REPLHistoryCache + vScrollToEnd replScroll gameState . replStatus .= REPLDone (Just val) gameState . baseRobot . robotContext . at itName .= Just val gameState . replNextValueIndex %= (+ 1) @@ -856,23 +858,6 @@ updateUI = do uiState . uiScrollToEnd .= True pure True - -- Decide whether the info panel has more content scrolled off the - -- top and/or bottom, so we can draw some indicators to show it if - -- so. Note, because we only know the update size and position of - -- the viewport *after* it has been rendered, this means the top and - -- bottom indicators will only be updated one frame *after* the info - -- panel updates, but this isn't really that big of deal. - infoPanelUpdated <- do - mvp <- lookupViewport InfoViewport - case mvp of - Nothing -> return False - Just vp -> do - let topMore = (vp ^. vpTop) > 0 - botMore = (vp ^. vpTop + snd (vp ^. vpSize)) < snd (vp ^. vpContentSize) - oldTopMore <- uiState . uiMoreInfoTop <<.= topMore - oldBotMore <- uiState . uiMoreInfoBot <<.= botMore - return $ oldTopMore /= topMore || oldBotMore /= botMore - goalOrWinUpdated <- doGoalUpdates let redraw = @@ -880,7 +865,6 @@ updateUI = do || inventoryUpdated || replUpdated || logUpdated - || infoPanelUpdated || goalOrWinUpdated pure redraw @@ -1131,57 +1115,66 @@ runBaseTerm topCtx = -- | Handle a user input event for the REPL. handleREPLEventTyping :: BrickEvent Name AppEvent -> EventM Name AppState () handleREPLEventTyping = \case - Key V.KEnter -> do - s <- get - let topCtx = topContext s - repl = s ^. uiState . uiREPL - uinput = repl ^. replPromptText - - if not $ s ^. gameState . replWorking - then case repl ^. replPromptType of - CmdPrompt _ -> runBaseCode topCtx uinput - SearchPrompt hist -> - case lastEntry uinput hist of - Nothing -> uiState %= resetREPL "" (CmdPrompt []) - Just found - | T.null uinput -> uiState %= resetREPL "" (CmdPrompt []) - | otherwise -> do - uiState %= resetREPL found (CmdPrompt []) - modify validateREPLForm - else continueWithoutRedraw - Key V.KUp -> modify $ adjReplHistIndex Older - Key V.KDown -> modify $ adjReplHistIndex Newer - ControlChar 'r' -> do - s <- get - let uinput = s ^. uiState . uiREPL . replPromptText - case s ^. uiState . uiREPL . replPromptType of - CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory) - SearchPrompt rh -> case lastEntry uinput rh of - Nothing -> pure () - Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh) - CharKey '\t' -> do - s <- get - let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1 - uiState . uiREPL %= tabComplete names (s ^. gameState . entityMap) - modify validateREPLForm - EscapeKey -> do - formSt <- use $ uiState . uiREPL . replPromptType - case formSt of - CmdPrompt {} -> continueWithoutRedraw - SearchPrompt _ -> - uiState %= resetREPL "" (CmdPrompt []) - ControlChar 'd' -> do - text <- use $ uiState . uiREPL . replPromptText - if text == T.empty - then toggleModal QuitModal - else continueWithoutRedraw - -- finally if none match pass the event to the editor - ev -> do - Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev) - uiState . uiREPL . replPromptType %= \case - CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor - SearchPrompt a -> SearchPrompt a - modify validateREPLForm + -- Scroll the REPL on PageUp or PageDown + Key V.KPageUp -> vScrollPage replScroll Brick.Up + Key V.KPageDown -> vScrollPage replScroll Brick.Down + k -> do + -- On any other key event, jump to the bottom of the REPL then handle the event + vScrollToEnd replScroll + case k of + Key V.KEnter -> do + s <- get + let topCtx = topContext s + repl = s ^. uiState . uiREPL + uinput = repl ^. replPromptText + + if not $ s ^. gameState . replWorking + then case repl ^. replPromptType of + CmdPrompt _ -> do + runBaseCode topCtx uinput + invalidateCacheEntry REPLHistoryCache + SearchPrompt hist -> + case lastEntry uinput hist of + Nothing -> uiState %= resetREPL "" (CmdPrompt []) + Just found + | T.null uinput -> uiState %= resetREPL "" (CmdPrompt []) + | otherwise -> do + uiState %= resetREPL found (CmdPrompt []) + modify validateREPLForm + else continueWithoutRedraw + Key V.KUp -> modify $ adjReplHistIndex Older + Key V.KDown -> modify $ adjReplHistIndex Newer + ControlChar 'r' -> do + s <- get + let uinput = s ^. uiState . uiREPL . replPromptText + case s ^. uiState . uiREPL . replPromptType of + CmdPrompt _ -> uiState . uiREPL . replPromptType .= SearchPrompt (s ^. uiState . uiREPL . replHistory) + SearchPrompt rh -> case lastEntry uinput rh of + Nothing -> pure () + Just found -> uiState . uiREPL . replPromptType .= SearchPrompt (removeEntry found rh) + CharKey '\t' -> do + s <- get + let names = s ^.. gameState . baseRobot . robotContext . defTypes . to assocs . traverse . _1 + uiState . uiREPL %= tabComplete names (s ^. gameState . entityMap) + modify validateREPLForm + EscapeKey -> do + formSt <- use $ uiState . uiREPL . replPromptType + case formSt of + CmdPrompt {} -> continueWithoutRedraw + SearchPrompt _ -> + uiState %= resetREPL "" (CmdPrompt []) + ControlChar 'd' -> do + text <- use $ uiState . uiREPL . replPromptText + if text == T.empty + then toggleModal QuitModal + else continueWithoutRedraw + -- finally if none match pass the event to the editor + ev -> do + Brick.zoom (uiState . uiREPL . replPromptEditor) (handleEditorEvent ev) + uiState . uiREPL . replPromptType %= \case + CmdPrompt _ -> CmdPrompt [] -- reset completions on any event passed to editor + SearchPrompt a -> SearchPrompt a + modify validateREPLForm data CompletionType = FunctionName diff --git a/src/Swarm/TUI/Model.hs b/src/Swarm/TUI/Model.hs index e2d430350..6a0129f36 100644 --- a/src/Swarm/TUI/Model.hs +++ b/src/Swarm/TUI/Model.hs @@ -78,6 +78,7 @@ module Swarm.TUI.Model ( populateInventoryList, infoScroll, modalScroll, + replScroll, -- * Runtime state RuntimeState, @@ -186,6 +187,9 @@ infoScroll = viewportScroll InfoViewport modalScroll :: ViewportScroll Name modalScroll = viewportScroll ModalViewport +replScroll :: ViewportScroll Name +replScroll = viewportScroll REPLViewport + -- ---------------------------------------------------------------------------- -- Runtime state -- -- ---------------------------------------------------------------------------- diff --git a/src/Swarm/TUI/Model/Name.hs b/src/Swarm/TUI/Model/Name.hs index cf2bd8767..9d6be71ff 100644 --- a/src/Swarm/TUI/Model/Name.hs +++ b/src/Swarm/TUI/Model/Name.hs @@ -64,6 +64,8 @@ data Name WorldEditorPanelControl WorldEditorFocusable | -- | The REPL input form. REPLInput + | -- | The REPL history cache. + REPLHistoryCache | -- | The render cache for the world view. WorldCache | -- | The cached extent for the world view. @@ -97,6 +99,8 @@ data Name InfoViewport | -- | The scrollable viewport for any modal dialog. ModalViewport + | -- | The scrollable viewport for the REPL. + REPLViewport | -- | A clickable button in a modal dialog. Button Button deriving (Eq, Ord, Show, Read) diff --git a/src/Swarm/TUI/Model/Repl.hs b/src/Swarm/TUI/Model/Repl.hs index d1ebfebc7..474103f10 100644 --- a/src/Swarm/TUI/Model/Repl.hs +++ b/src/Swarm/TUI/Model/Repl.hs @@ -21,6 +21,7 @@ module Swarm.TUI.Model.Repl ( addREPLItem, restartREPLHistory, getLatestREPLHistoryItems, + getSessionREPLHistoryItems, moveReplHistIndex, getCurrentItemText, replIndexIsAtInput, @@ -185,6 +186,11 @@ getLatestREPLHistoryItems n h = toList latestN latestN = Seq.drop oldestIndex $ h ^. replSeq oldestIndex = max (h ^. replStart) $ length (h ^. replSeq) - n +-- | Get only the items from the REPL history that were entered during +-- the current session. +getSessionREPLHistoryItems :: REPLHistory -> Seq REPLHistItem +getSessionREPLHistoryItems h = Seq.drop (h ^. replStart) (h ^. replSeq) + data TimeDir = Newer | Older deriving (Eq, Ord, Show) moveReplHistIndex :: TimeDir -> Text -> REPLHistory -> REPLHistory diff --git a/src/Swarm/TUI/Model/UI.hs b/src/Swarm/TUI/Model/UI.hs index cfe3f4bdc..3a2fb2ec7 100644 --- a/src/Swarm/TUI/Model/UI.hs +++ b/src/Swarm/TUI/Model/UI.hs @@ -20,8 +20,6 @@ module Swarm.TUI.Model.UI ( uiInventory, uiInventorySort, uiInventorySearch, - uiMoreInfoTop, - uiMoreInfoBot, uiScrollToEnd, uiError, uiModal, @@ -107,8 +105,6 @@ data UIState = UIState , _uiInventory :: Maybe (Int, BL.List Name InventoryListEntry) , _uiInventorySort :: InventorySortOptions , _uiInventorySearch :: Maybe Text - , _uiMoreInfoTop :: Bool - , _uiMoreInfoBot :: Bool , _uiScrollToEnd :: Bool , _uiError :: Maybe Text , _uiModal :: Maybe Modal @@ -177,12 +173,6 @@ uiInventorySearch :: Lens' UIState (Maybe Text) -- focused robot's inventory. uiInventory :: Lens' UIState (Maybe (Int, BL.List Name InventoryListEntry)) --- | Does the info panel contain more content past the top of the panel? -uiMoreInfoTop :: Lens' UIState Bool - --- | Does the info panel contain more content past the bottom of the panel? -uiMoreInfoBot :: Lens' UIState Bool - -- | A flag telling the UI to scroll the info panel to the very end -- (used when a new log message is appended). uiScrollToEnd :: Lens' UIState Bool @@ -329,8 +319,6 @@ initUIState speedFactor showMainMenu cheatMode = do , _uiInventory = Nothing , _uiInventorySort = defaultSortOptions , _uiInventorySearch = Nothing - , _uiMoreInfoTop = False - , _uiMoreInfoBot = False , _uiScrollToEnd = False , _uiError = Nothing , _uiModal = Nothing diff --git a/src/Swarm/TUI/View.hs b/src/Swarm/TUI/View.hs index faac91c8a..a1c30e3d4 100644 --- a/src/Swarm/TUI/View.hs +++ b/src/Swarm/TUI/View.hs @@ -51,6 +51,7 @@ import Control.Lens as Lens hiding (Const, from) import Control.Monad (guard) import Data.Array (range) import Data.Bits (shiftL, shiftR, (.&.)) +import Data.Foldable (toList) import Data.Foldable qualified as F import Data.Functor (($>)) import Data.IntMap qualified as IM @@ -103,7 +104,7 @@ import Swarm.TUI.Launch.Model import Swarm.TUI.Launch.View import Swarm.TUI.Model import Swarm.TUI.Model.Goal (goalsContent, hasAnythingToShow) -import Swarm.TUI.Model.Repl (lastEntry) +import Swarm.TUI.Model.Repl (getSessionREPLHistoryItems, lastEntry) import Swarm.TUI.Model.UI import Swarm.TUI.Panel import Swarm.TUI.View.Achievement @@ -401,12 +402,7 @@ drawGameUI s = highlightAttr fr (FocusablePanel InfoPanel) - ( plainBorder - & topLabels . centerLabel - .~ (if moreTop then Just (txt " · · · ") else Nothing) - & bottomLabels . centerLabel - .~ (if moreBot then Just (txt " · · · ") else Nothing) - ) + plainBorder $ drawInfoPanel s , hCenter . clickable (FocusablePanel WorldEditorPanel) @@ -426,8 +422,6 @@ drawGameUI s = -- has a clock equipped addClock = topLabels . rightLabel ?~ padLeftRight 1 (drawClockDisplay (s ^. uiState . lgTicksPerSecond) $ s ^. gameState) fr = s ^. uiState . uiFocusRing - moreTop = s ^. uiState . uiMoreInfoTop - moreBot = s ^. uiState . uiMoreInfoBot showREPL = s ^. uiState . uiShowREPL rightPanel = if showREPL then worldPanel ++ replPanel else worldPanel ++ minimizedREPL minimizedREPL = case focusGetCurrent fr of @@ -458,7 +452,7 @@ drawGameUI s = ) ( vLimit replHeight . padBottom Max - . padLeftRight 1 + . padLeft (Pad 1) $ drawREPL s ) ] @@ -885,10 +879,11 @@ drawKeyMenu :: AppState -> Widget Name drawKeyMenu s = vLimit 2 $ hBox - [ vBox - [ mkCmdRow globalKeyCmds - , padLeft (Pad 2) contextCmds - ] + [ padBottom Max $ + vBox + [ mkCmdRow globalKeyCmds + , padLeft (Pad 2) contextCmds + ] , gameModeWidget ] where @@ -950,9 +945,7 @@ drawKeyMenu s = ] may b = if b then Just else const Nothing - highlightKeyCmds (k, n) = (,k,n) $ case n of - "pop out" | (s ^. uiState . uiMoreInfoBot) || (s ^. uiState . uiMoreInfoTop) -> Alert - _ -> PanelSpecific + highlightKeyCmds (k, n) = (PanelSpecific, k, n) keyCmdsFor (Just (FocusablePanel WorldEditorPanel)) = [("^s", "save map")] @@ -963,6 +956,7 @@ drawKeyMenu s = ++ [("^c", "cancel") | isReplWorking] ++ [("M-p", renderPilotModeSwitch ctrlMode) | creative] ++ [("M-k", renderHandlerModeSwitch ctrlMode) | handlerInstalled] + ++ [("PgUp/Dn", "scroll")] keyCmdsFor (Just (FocusablePanel WorldPanel)) = [ ("←↓↑→ / hjkl", "scroll") | canScroll ] @@ -1040,7 +1034,8 @@ drawRobotPanel s in padBottom Max $ vBox [ hCenter $ hBox row - , padAll 1 (BL.renderListWithIndex drawClickableItem True lst) + , withLeftPaddedVScrollBars . padLeft (Pad 1) . padTop (Pad 1) $ + BL.renderListWithIndex drawClickableItem True lst ] | otherwise = blank @@ -1091,7 +1086,8 @@ drawInfoPanel :: AppState -> Widget Name drawInfoPanel s | Just Far <- s ^. gameState . to focusedRange = blank | otherwise = - viewport InfoViewport Vertical + withVScrollBars OnRight + . viewport InfoViewport Vertical . padLeftRight 1 $ explainFocusedItem s @@ -1333,20 +1329,43 @@ renderREPLPrompt focus repl = ps1 <+> replE -- | Draw the REPL. drawREPL :: AppState -> Widget Name -drawREPL s = vBox $ latestHistory <> [currentPrompt] <> mayDebug +drawREPL s = + vBox + [ withLeftPaddedVScrollBars + . viewport REPLViewport Vertical + . vBox + $ [cached REPLHistoryCache (vBox history), currentPrompt] + , vBox mayDebug + ] where -- rendered history lines fitting above REPL prompt - latestHistory :: [Widget n] - latestHistory = map fmt (getLatestREPLHistoryItems (replHeight - inputLines - debugLines) (repl ^. replHistory)) + history :: [Widget n] + history = map fmt . toList . getSessionREPLHistoryItems $ repl ^. replHistory currentPrompt :: Widget Name currentPrompt = case (isActive <$> base, repl ^. replControlMode) of (_, Handling) -> padRight Max $ txt "[key handler running, M-k to toggle]" (Just False, _) -> renderREPLPrompt (s ^. uiState . uiFocusRing) repl _running -> padRight Max $ txt "..." - inputLines = 1 - debugLines = 3 * fromEnum (s ^. uiState . uiShowDebug) repl = s ^. uiState . uiREPL base = s ^. gameState . robotMap . at 0 fmt (REPLEntry e) = txt $ "> " <> e fmt (REPLOutput t) = txt t mayDebug = [drawRobotMachine s True | s ^. uiState . uiShowDebug] + +------------------------------------------------------------ +-- Utility +------------------------------------------------------------ + +-- See https://github.com/jtdaugherty/brick/discussions/484 +withLeftPaddedVScrollBars :: Widget n -> Widget n +withLeftPaddedVScrollBars = + withVScrollBarRenderer (addLeftSpacing verticalScrollbarRenderer) + . withVScrollBars OnRight + where + addLeftSpacing :: VScrollbarRenderer n -> VScrollbarRenderer n + addLeftSpacing r = + r + { scrollbarWidthAllocation = 2 + , renderVScrollbar = hLimit 1 $ renderVScrollbar r + , renderVScrollbarTrough = hLimit 1 $ renderVScrollbarTrough r + } diff --git a/stack.yaml b/stack.yaml index 5a38d0f9e..a02ca2be8 100644 --- a/stack.yaml +++ b/stack.yaml @@ -3,7 +3,8 @@ extra-deps: - hsnoise-0.0.3@sha256:260b39175b8a3e3b1719ad3987b7d72a3fd7a0fa99be8639b91cf4dc3f1c8796,1476 - simple-enumeration-0.2.1@sha256:8625b269c1650d3dd0e3887351c153049f4369853e0d525219e07480ea004b9f,1178 - boolexpr-0.2@sha256:07f38a0206ad63c2c893e3c6271a2e45ea25ab4ef3a9e973edc746876f0ab9e8,853 -- brick-list-skip-0.1.1.4 +- brick-1.10 +- brick-list-skip-0.1.1.5 # We should update to lsp-2.0 and lsp-types-2.0 but it involves some # breaking changes; see https://github.com/swarm-game/swarm/issues/1350 - lsp-1.6.0.0 diff --git a/swarm.cabal b/swarm.cabal index 0aec8d667..cdb1f9d99 100644 --- a/swarm.cabal +++ b/swarm.cabal @@ -213,7 +213,7 @@ library array >= 0.5.4 && < 0.6, blaze-html >= 0.9.1 && < 0.9.2, boolexpr >= 0.2 && < 0.3, - brick >= 1.5 && < 1.10, + brick >= 1.10 && < 1.11, bytestring >= 0.10 && < 0.12, clock >= 0.8.2 && < 0.9, colour >= 2.3.6 && < 2.4,