ghcitui-0.4.0.0: lib/ghcitui-brick/Ghcitui/Brick/InterpWindowEvents.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE NamedFieldPuns #-}
module Ghcitui.Brick.InterpWindowEvents
( handleInterpreterEvent
, handleInterpWindowPostCb
) where
import qualified Brick.BChan as B
import qualified Brick.Main as B
import qualified Brick.Types as B
import qualified Brick.Widgets.Edit as BE
import Control.Error (atDef, lastDef)
import Control.Monad.IO.Class (MonadIO (..))
import qualified Data.Text as T
import qualified Data.Text.Zipper as T
import qualified Graphics.Vty as V
import Lens.Micro ((^.))
import qualified Lens.Micro as Lens
import qualified Ghcitui.Brick.AppInterpState as AIS
import Ghcitui.Brick.AppState as AppState
import Ghcitui.Brick.AppTopLevel
( AppName (..)
, CustomAppEvent (..)
)
import Ghcitui.Brick.EventUtils
( commonPrefixes
, reflowText
)
import qualified Ghcitui.Ghcid.Daemon as Daemon
import Ghcitui.Util (showT)
-- -------------------------------------------------------------------------------------------------
-- Interpreter Event Handling
-- -------------------------------------------------------------------------------------------------
-- | Handle events when the interpreter (live GHCi/REPL) is selected.
handleInterpreterEvent :: B.BrickEvent AppName e -> B.EventM AppName (AppState AppName) ()
handleInterpreterEvent ev = do
appState <- B.get
case ev of
B.VtyEvent (V.EvKey V.KEnter []) -> do
let cmd = T.strip (T.unlines (editorContents appState))
let finishedState = appState{waitingOnRepl = False}
let callback = replExecCb cmd finishedState
-- While the command is running, put write some temporary confirmation that the command
-- was run to the logs.
B.put
. appendToLogs mempty cmd
. replaceCommandBuffer ""
$ appState{waitingOnRepl = True}
B.invalidateCache
-- Actually schedule the command.
let interpState = AppState.interpState appState
liftIO $
Daemon.scheduleWithCb interpState (Daemon.execCleaned cmd interpState) callback
B.VtyEvent (V.EvKey (V.KChar '\t') []) -> do
-- We want to preserve spaces, but not trailing newlines.
let cmd = T.dropWhileEnd ('\n' ==) . T.unlines . editorContents $ appState
let callback = tabCompleteCb cmd appState
let interpState = AppState.interpState appState
B.put appState
-- Schedule the tab completion.
liftIO $
Daemon.scheduleWithCb interpState (Daemon.tabComplete cmd interpState) callback
B.VtyEvent (V.EvKey (V.KChar 'x') [V.MCtrl]) ->
-- Toggle out of the interpreter.
leaveInterpreter
B.VtyEvent (V.EvKey V.KEsc _) -> do
if not $ appState ^. appInterpState . AIS.viewLock
then -- Exit scroll mode first.
B.put (Lens.set (appInterpState . AIS.viewLock) True appState)
else -- Also toggle out of the interpreter.
leaveInterpreter
-- Selecting previous commands.
B.VtyEvent (V.EvKey V.KUp _) -> do
let maybeStoreBuffer s =
if not (AIS.isScanningHist (getAis s))
then storeCommandBuffer s
else s
let wDebug s =
writeDebugLog
( "handled Up; historyPos is "
<> (showT . AIS.historyPos . getAis $ s)
)
s
let appState' =
wDebug
. replaceCommandBufferWithHist -- Display the history.
. Lens.over appInterpState AIS.pastHistoryPos -- Go back in time.
. maybeStoreBuffer -- Store the buffer if we're not scanning already.
$ appState
B.put appState'
B.VtyEvent (V.EvKey V.KDown _) -> do
let wDebug s =
writeDebugLog
( "handled Down; historyPos is "
<> (showT . AIS.historyPos . getAis $ s)
)
s
let appState' =
wDebug
. replaceCommandBufferWithHist -- Display the history.
. Lens.over appInterpState AIS.futHistoryPos -- Go forward in time.
$ appState
B.put appState'
-- Scrolling back through the logs.
B.VtyEvent (V.EvKey V.KPageDown _) ->
B.vScrollPage (B.viewportScroll LiveInterpreterViewport) B.Down
B.VtyEvent (V.EvKey V.KPageUp _) -> do
B.vScrollPage (B.viewportScroll LiveInterpreterViewport) B.Up
B.put (Lens.set (appInterpState . AIS.viewLock) False appState)
B.VtyEvent (V.EvKey (V.KChar 'n') [V.MCtrl]) -> do
-- Invert the viewLock.
B.put (Lens.over (appInterpState . AIS.viewLock) not appState)
-- While scrolling (viewLock disabled), allow resizing the live interpreter history.
B.VtyEvent (V.EvKey (V.KChar '+') [])
| not (appState ^. appInterpState . AIS.viewLock) -> do
B.put (AppState.changeReplWidgetSize 1 appState)
B.VtyEvent (V.EvKey (V.KChar '-') [])
| not (appState ^. appInterpState . AIS.viewLock) -> do
B.put (AppState.changeReplWidgetSize (-1) appState)
-- Actually handle keystrokes.
ev' ->
if waitingOnRepl appState
then
-- Don't print a prompt if we're waiting.
pure ()
else do
-- When typing, bring us back down to the terminal.
B.put (Lens.set (appInterpState . AIS.viewLock) True appState)
-- Actually handle text input commands.
B.zoom liveEditor $ BE.handleEditorEvent ev'
where
editorContents appState = BE.getEditContents $ appState ^. liveEditor
storeCommandBuffer appState =
Lens.set (appInterpState . AIS.commandBuffer) (editorContents appState) appState
getAis s = s ^. appInterpState
getCommandAtHist :: Int -> AppState n -> [T.Text]
getCommandAtHist i s
| i <= 0 = s ^. appInterpState . AIS.commandBuffer
| otherwise = atDef (lastDef [] hist) hist (i - 1)
where
hist = s ^. appInterpState . Lens.to AIS.cmdHistory
leaveInterpreter = B.put . toggleActiveLineInterpreter =<< B.get
replaceCommandBufferWithHist :: AppState n -> AppState n
replaceCommandBufferWithHist s@AppState{_appInterpState} = replaceCommandBuffer cmd s
where
cmd = T.unlines . getCommandAtHist (AIS.historyPos _appInterpState) $ s
appendToLogs
:: [T.Text]
-- ^ Logs between commands.
-> T.Text
-- ^ The command sent to produce the logs.
-> AppState n
-- ^ State to update.
-> AppState n
-- ^ Updated state.
appendToLogs logs promptEntry state = state{interpLogs = take interpreterLogLimit combinedLogs}
where
combinedLogs = reverse logs <> (formattedWithPrompt : interpLogs state)
formattedWithPrompt = getInterpreterPrompt (appConfig state) <> promptEntry
-- TODO: Should be configurable?
interpreterLogLimit = 1000
-- | Replace the command buffer with the given strings of Text.
replaceCommandBuffer
:: T.Text
-- ^ Text to replace with.
-> AppState n
-- ^ State to modify.
-> AppState n
-- ^ New state.
replaceCommandBuffer replacement s = Lens.set liveEditor newEditor s
where
zipp :: T.TextZipper T.Text -> T.TextZipper T.Text
zipp = T.killToEOF . T.insertMany replacement . T.gotoBOF
newEditor = BE.applyEdit zipp (s ^. liveEditor)
-- -------------------------------------------------------------------------------------------------
-- Callbacks and Callback utils
-- -------------------------------------------------------------------------------------------------
-- | Live Interpreter/REPL Callback. Called asynchronously after the 'DaemonIO' resolves.
replExecCb
:: T.Text
-- ^ Command sent and ran on the Daemon.
-> AppState n
-- ^ 'AppState' to use for asynchronous channel communication.
-> Either Daemon.DaemonError (Daemon.InterpState (), [T.Text])
-- ^ The incoming response from the Daemon for the 'step' (or similar) operation.
-> IO ()
-- ^ IO used to write to the event bounded channel.
replExecCb cmd appState (Right (interpState, logs)) =
B.writeBChan (AppState.eventChannel appState) (ReplExecCb appState{interpState} cmd logs)
replExecCb _ appState (Left msg) =
B.writeBChan (AppState.eventChannel appState) (ErrorOnCb appState (showT msg))
tabCompleteCb
:: T.Text
-- ^ Partial command to get completion of.
-> AppState n
-> Either Daemon.DaemonError (Daemon.InterpState (), (T.Text, [T.Text]))
-> IO ()
tabCompleteCb cmd appState (Right (interpState, (prefix, completions))) =
B.writeBChan
(AppState.eventChannel appState)
(ReplTabCompleteCb appState{interpState} cmd (prefix, completions))
tabCompleteCb _ appState (Left msg) =
B.writeBChan (AppState.eventChannel appState) (ErrorOnCb appState (showT msg))
-- | Synchronous code to update the state after a InterpreterEvent callback.
handleInterpWindowPostCb
:: CustomAppEvent (AppState AppName) -> B.EventM AppName (AppState AppName) ()
handleInterpWindowPostCb (ReplExecCb appState cmd newLogs) = do
let newEditor =
BE.applyEdit
(T.killToEOF . T.gotoBOF)
(appState ^. liveEditor)
let newAppState2 =
writeDebugLog ("handled Enter: Ran '" <> cmd <> "'")
. Lens.set (appInterpState . AIS.viewLock) True
. Lens.over appInterpState (AIS.pushHistory [cmd])
$ appendToLogs newLogs cmd appState
let appStateFinalIO = updateSourceMap (Lens.set liveEditor newEditor newAppState2)
B.put =<< liftIO appStateFinalIO
-- Invalidate the entire render state of the application
-- because we don't know what's actually changed here now.
B.invalidateCache
handleInterpWindowPostCb (ReplTabCompleteCb appState cmd (prefix, completions)) = do
let maxCompletionLen = maximum $ T.length <$> completions
let columnPadding = 1
extent <-
B.lookupExtent LiveInterpreterViewport >>= \case
Just e -> pure e
Nothing -> error "Could not find extent of LiveInterpreterViewport"
let interpWidth = fst . B.extentSize $ extent
let completionColWidth = min (interpWidth - 2) maxCompletionLen + columnPadding
let numCols = interpWidth `div` completionColWidth
let updateCompletions cs s = case cs of
-- Only one completion, just replace the entire buffer with it.
[c] -> replaceCommandBuffer (prefix <> c <> " ") s
-- No completions. Just go to a new prompt.
[] -> appendToLogs [] cmd s
-- Replace the buffer with the longest possible prefix among options, and
-- print the remaining.
_ ->
replaceCommandBuffer (prefix <> commonPrefixes cs)
. appendToLogs (reflowText numCols completionColWidth cs) cmd
$ s
B.put
. writeDebugLog
( "handled Tab, Prefix was: '"
<> cmd
<> "' completions were: "
<> showT completions
)
. updateCompletions completions
$ appState
-- For all other AppEvent types, ignore them. They're handled elsewhere.
handleInterpWindowPostCb _ = pure ()