packages feed

haskell-debugger-0.13.0.0: hdb/Development/Debug/Adapter/Stopped.hs

{-# LANGUAGE RecordWildCards, OverloadedRecordDot, OverloadedStrings, LambdaCase #-}

-- | Getting information about where we're stopped at (current suspended state).
--
-- Includes the commands to execute the following requests on the debuggee state:
-- 
-- @
-- Threads
--    StackTrace
--       Scopes
--          Variables
--             ...
--                Variables
-- @
module Development.Debug.Adapter.Stopped where

import Control.Monad
import qualified Data.IntMap as IM
import qualified Data.Text as T

import DAP

import GHC.Debugger.Interface.Messages
import Development.Debug.Adapter
import Development.Debug.Adapter.Interface

--------------------------------------------------------------------------------
-- * StackTrace
--------------------------------------------------------------------------------

-- | Command to get thread information at current stopped point
commandThreads :: DebugAdaptor ()
commandThreads = do
  GotThreads ts <- sendSync GetThreads
  sendThreadsResponse $
    map (\t ->
      Thread
        { threadId    = remoteThreadIntRef t.tId
        , threadName  = maybe (T.pack $ "Thread #" ++ show (remoteThreadIntRef t.tId)) T.pack t.tName
        }
      ) ts

--------------------------------------------------------------------------------
-- * StackTrace
--------------------------------------------------------------------------------

-- | Command to fetch stack trace at current stop point
commandStackTrace :: DebugAdaptor ()
commandStackTrace = do
  StackTraceArguments{..} <- getArguments
  let threadId = RemoteThreadId stackTraceArgumentsThreadId
  GotStacktrace stackFrames <- sendSync (GetStacktrace threadId)
  (responseFrames, newStackFrameMap) <- fmap (unzip . concat) $
    forM (zip stackFrames [0..]) $ \(stackFrame, stackFrameIx) -> do
      freshId <- getFreshId
      source <- fileToSource stackFrame.sourceSpan.file
      let responseFrame = defaultStackFrame
            { stackFrameId = freshId
            , stackFrameName = T.pack stackFrame.name
            , stackFrameLine = stackFrame.sourceSpan.startLine
            , stackFrameColumn = stackFrame.sourceSpan.startCol
            , stackFrameEndLine = Just stackFrame.sourceSpan.endLine
            , stackFrameEndColumn = Just stackFrame.sourceSpan.endCol
            , stackFrameSource = Just source
            }
      let newMapEntry = (freshId, StackFrameIx threadId stackFrameIx)
      return [(responseFrame, newMapEntry)]

  updateDebugSession (\s -> s { stackFrameMap = s.stackFrameMap <> IM.fromList newStackFrameMap })

  sendStackTraceResponse StackTraceResponse
    { stackFrames = responseFrames
    , totalFrames = if null responseFrames then Nothing else Just (length responseFrames)
    }

--------------------------------------------------------------------------------
-- * Scopes
--------------------------------------------------------------------------------

-- | Command to get scopes for current stopped point
commandScopes :: DebugAdaptor ()
commandScopes = do
  ScopesArguments{..} <- getArguments
  let frameId = scopesArgumentsFrameId
  sfMap <- stackFrameMap <$> getDebugSession
  case IM.lookup frameId sfMap of
    Nothing -> do
      sendErrorResponse (ErrorMessage (T.pack $ "Could not find stack frame for id " ++ show frameId)) Nothing
    Just six@(StackFrameIx threadId frameIx) -> do
      GotScopes scopes <- sendSync (GetScopes threadId frameIx)
      sendScopesResponse . ScopesResponse =<<
        mapM (scopeInfoToScope six) scopes

-- | 'ScopeInfo' to 'Scope'
scopeInfoToScope :: StackFrameIx -> ScopeInfo -> DebugAdaptor Scope
scopeInfoToScope six ScopeInfo{..} = do

  -- Update vars map
  varId <- freshVarIx six (scopeToVarRef kind)

  source <- fileToSource sourceSpan.file
  return Scope
    { scopeName = case kind of
        LocalVariablesScope -> "Locals"
        ModuleVariablesScope -> "Module"
        GlobalVariablesScope -> "Globals"
    , scopePresentationHint = Just $ case kind of
        LocalVariablesScope -> ScopePresentationHintLocals
        ModuleVariablesScope -> ScopePresentationHint "module"
        GlobalVariablesScope -> ScopePresentationHint "globals"
    , scopeNamedVariables = numVars
    , scopeIndexedVariables = Nothing
    , scopeSource = Just source
    , scopeLine = Just sourceSpan.startLine
    , scopeColumn = Just sourceSpan.startCol
    , scopeEndLine = Just sourceSpan.endLine
    , scopeEndColumn = Just sourceSpan.endCol
    , scopeVariablesReference = varId
    , scopeExpensive = expensive
    }

--------------------------------------------------------------------------------
-- * Variables
--------------------------------------------------------------------------------

-- | Command to get variables by reference number
commandVariables :: DebugAdaptor ()
commandVariables = do
  VariablesArguments{..} <- getArguments

  vsMap <- variablesMap <$> getDebugSession
  case IM.lookup variablesArgumentsVariablesReference vsMap of
    Nothing -> sendErrorResponse (ErrorMessage (T.pack $ "Could not find variable reference " ++ show variablesArgumentsVariablesReference)) Nothing
    Just (VariablesIx six@(StackFrameIx threadId frameIx) varRef) -> do
      GotVariables vars <- sendSync (GetVariables threadId frameIx varRef)
      sendVariablesResponse . VariablesResponse =<<
        mapM (varInfoToVariables six) (variableResultToList vars)
      case vars of
        -- If the reply indicates this was an "inspect lazy variable" request
        -- (because the requested variable was forced instead of returning an
        -- expansion), invalidate the parent variables.
        --
        -- The client side seems to handle rendering only the bits which changed
        -- out very well, while preserving the variable tree expansion.
        -- In any case, we might have to pessimistically redo all variable
        -- responses because any value may be changed by an updated thunk, not only
        -- the parent variables.
        ForcedVariable _
          -> sendInvalidatedEvent defaultInvalidatedEvent
              { invalidatedEventAreas = [InvalidatedAreasVariables]
              , invalidatedEventStackFrameId = Just 0 -- TODO: REVERSE LOOKUP OF (StackFrameIx threadId frameIx)
              }
        VariableFields _ -> return ()

-- | 'VarInfo' to 'Variable's.
varInfoToVariables :: StackFrameIx -> VarInfo -> DebugAdaptor Variable
varInfoToVariables six VarInfo{..} = do
  varId <- freshVarIx six varRef

  return defaultVariable
    { variableName = T.pack varName
    , variableValue = T.pack varValue
    , variableType = Just $ T.pack varType
    , variableEvaluateName = Just $ T.pack varName
    , variableVariablesReference = varId
    , variableNamedVariables = Nothing
    , variableIndexedVariables = Nothing
    , variablePresentationHint = Just defaultVariablePresentationHint
        { variablePresentationHintLazy = Just isThunk
        }
    }

--------------------------------------------------------------------------------
-- Variable ix references
--------------------------------------------------------------------------------

freshVarIx :: StackFrameIx -> VariableReference -> DebugAdaptor Int
freshVarIx _ NoVariables = pure 0 -- No variables means the reference should be 0, e.g. denoting the variable is not expandable
freshVarIx six vr = do
  varId <- getFreshId
  updateDebugSession (\s -> s { variablesMap = IM.insert varId (VariablesIx six vr) s.variablesMap })
  return varId