packages feed

ghcitui-0.1.0.0: lib/ghcitui-brick/Ghcitui/Brick/DrawSourceViewer.hs

{-# LANGUAGE NamedFieldPuns #-}

module Ghcitui.Brick.DrawSourceViewer (drawSourceViewer) where

import qualified Brick as B
import qualified Brick.Widgets.Center as B
import Brick.Widgets.Core ((<+>), (<=>))
import Control.Error (fromMaybe)
import Data.Function ((&))
import Data.Functor ((<&>))
import qualified Data.Text as T
import qualified Data.Vector as Vec
import qualified Graphics.Vty as V
import Lens.Micro ((^.))

import Ghcitui.Brick.AppState (AppState)
import qualified Ghcitui.Brick.AppState as AppState
import Ghcitui.Brick.AppTopLevel (AppName (..))
import qualified Ghcitui.Brick.SourceWindow as SourceWindow
import qualified Ghcitui.Ghcid.Daemon as Daemon
import qualified Ghcitui.Loc as Loc
import qualified Ghcitui.Util as Util

-- | Make the primary viewport widget.
drawSourceViewer :: AppState AppName -> B.Widget AppName
drawSourceViewer s
    | (srcWindow ^. SourceWindow.srcElementsL) /= mempty = drawSourceViewer' s srcWindow
    | not currentlyRunning = notRunningWidget
    | otherwise = noSourceWidget
  where
    currentlyRunning = Daemon.isExecuting (AppState.interpState s)
    srcWindow = s ^. AppState.sourceWindow
    notRunningWidget =
        padWidget splashWidget
            <=> padWidget (B.txt "Nothing executing. Maybe run something?")
    noSourceWidget =
        padWidget splashWidget <=> padWidget (B.txt "Can't display. Source not found.")
    padWidget w =
        B.padTop (B.Pad 3)
            . B.hCenter
            $ B.withAttr (B.attrName "styled") w
    splashWidget = B.txt $ fromMaybe "No splash file loaded." (AppState.splashContents s)

-- -------------------------------------------------------------------------------------------------
-- Source Viewer Drawing Details
-- -------------------------------------------------------------------------------------------------

-- | Information used to compute the gutter status of each line.
data GutterInfo = GutterInfo
    { isStoppedHere :: !Bool
    -- ^ Is the interpreter stopped/paused here?
    , isBreakpoint :: !Bool
    -- ^ Is there a breakpoint here?
    , isSelected :: !Bool
    -- ^ Is this line currently selected by the user?
    , gutterLineNumber :: !Int
    -- ^ What line number is this?
    , gutterDigitWidth :: !Int
    -- ^ How many columns is the gutter line number?
    }

-- | Prepend gutter information on each line in the primary viewport.
prependGutter :: GutterInfo -> B.Widget n -> B.Widget n
prependGutter gi line = makeGutter gi <+> line

{- | Create the gutter section for a given line (formed from GutterInfo).
     This should be cached wherever since there can be thousands of these
     in a source.
-}
makeGutter :: GutterInfo -> B.Widget n
makeGutter GutterInfo{..} =
    lineNoWidget <+> spaceW <+> stopColumn <+> breakColumn <+> spaceW
  where
    spaceW = B.txt " "
    lineNoWidget =
        let attr = B.attrName (if isSelected then "selected-line-numbers" else "line-numbers")
         in B.withAttr attr (B.txt (Util.formatDigits gutterDigitWidth gutterLineNumber))
    breakColumn
        | isSelected && isBreakpoint = B.withAttr (B.attrName "selected-marker") (B.txt "@")
        | isSelected = B.withAttr (B.attrName "selected-marker") (B.txt ">")
        | isBreakpoint = B.withAttr (B.attrName "breakpoint-marker") (B.txt "*")
        | otherwise = spaceW
    stopColumn
        | isStoppedHere = B.withAttr (B.attrName "stop-line") (B.txt "!")
        | otherwise = spaceW

-- | Panel when we have source contents.
drawSourceViewer'
    :: AppState AppName
    -> SourceWindow.SourceWindow AppName T.Text
    -> B.Widget AppName
drawSourceViewer' s sourceWindow = composedTogether
  where
    isSelectedLine :: Int -> Bool
    isSelectedLine lineno = Just lineno == sourceWindow ^. SourceWindow.srcSelectedLineL

    composedTogether :: B.Widget AppName
    composedTogether = SourceWindow.renderSourceWindow createWidget sourceWindow
      where
        createWidget lineno _old lineTxt =
            styliseLine $ composedTogetherHelper lineno lineTxt
          where
            styliseLine w =
                if isSelectedLine lineno
                    then -- Add highlighting, then mark it as visible in the viewport.
                        B.modifyDefAttr (`V.withStyle` V.bold) w
                    else w

    -- Select which line widget we want to draw based on both the interpreter
    -- state and the app state.
    --
    -- It's important that the line information is cached, because
    -- each line is actually pretty expensive to render.
    composedTogetherHelper :: Int -> T.Text -> B.Widget AppName
    composedTogetherHelper lineno lineTxt = lineWidgetCached
      where
        sr = maybe Loc.unknownSourceRange Loc.sourceRange (Daemon.pauseLoc (AppState.interpState s))
        mLineno = Loc.singleify sr
        lineWidget = case mLineno of
            -- This only makes the stopped line widget appear for the start loc.
            Just (singleLine, _) | lineno == singleLine -> stoppedLineW lineTxt
            -- If it's a range, just try to show the range.
            _
                | Loc.isLineInside sr lineno -> stoppedRangeW
            -- Default case (includes selected and non-selected).
            _ -> (\w -> prefixLine (lineno, w)) . B.txt $ lineTxt
        lineWidgetCached = B.cached (SourceWindowLine lineno) lineWidget

        stoppedRangeW :: B.Widget AppName
        stoppedRangeW =
            prefixLine
                ( lineno
                , B.forceAttrAllowStyle (B.attrName "stop-line") (B.txt lineTxt)
                )

    prefixLine :: (Int, B.Widget n) -> B.Widget n
    prefixLine (lineno', w) =
        prependGutter
            (gutterInfoForLine lineno')
            w
      where
        gutterInfoForLine :: Int -> GutterInfo
        gutterInfoForLine lineno =
            GutterInfo
                { isStoppedHere =
                    Daemon.pauseLoc (AppState.interpState s)
                        <&> Loc.sourceRange
                        <&> (`Loc.isLineInside` lineno)
                        & fromMaybe False
                , isBreakpoint = lineno `elem` breakpoints
                , gutterLineNumber = lineno
                , gutterDigitWidth = Util.getNumDigits $ sourceWindowLength sourceWindow
                , isSelected = isSelectedLine lineno
                }
          where
            breakpoints :: [Int]
            breakpoints =
                maybe
                    mempty
                    (\f -> Daemon.getBpInFile f (AppState.interpState s))
                    (AppState.selectedFile s)

    originalLookupLineNo :: Int
    originalLookupLineNo =
        Daemon.pauseLoc (AppState.interpState s)
            >>= Loc.startLine . Loc.sourceRange
            & fromMaybe 0

    stoppedLineW :: T.Text -> B.Widget AppName
    stoppedLineW lineTxt =
        let Loc.SourceRange{startCol, endCol} =
                maybe
                    Loc.unknownSourceRange
                    Loc.sourceRange
                    (Daemon.pauseLoc (AppState.interpState s))
            lineWidget = makeStoppedLineWidget lineTxt (startCol, endCol)
         in prefixLine (originalLookupLineNo, lineWidget)

sourceWindowLength :: SourceWindow.SourceWindow n e -> Int
sourceWindowLength = Vec.length . SourceWindow.srcElements

-- | Make the Stopped Line widget (the line where we paused execution)
makeStoppedLineWidget :: T.Text -> Loc.ColumnRange -> B.Widget AppName
makeStoppedLineWidget lineData (Nothing, _) =
    B.forceAttrAllowStyle (B.attrName "stop-line") (B.txt lineData)
makeStoppedLineWidget lineData (Just startCol, Nothing) =
    makeStoppedLineWidget lineData (Just startCol, Just (startCol + 1))
makeStoppedLineWidget lineData (Just startCol, Just endCol) =
    B.forceAttrAllowStyle
        (B.attrName "stop-line")
        ( B.txt lineDataBefore
            <+> B.withAttr (B.attrName "highlight") (B.txt lineDataRange)
            <+> B.txt lineDataAfter
        )
  where
    (lineDataBefore, partial) = T.splitAt (startCol - 1) lineData
    (lineDataRange, lineDataAfter) = T.splitAt (endCol - startCol + 1) partial