packages feed

futhark-0.25.37: src/Futhark/LSP/Tool.hs

{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PartialTypeSignatures #-}

-- | Generally useful definition used in various places in the
-- language server implementation.
module Futhark.LSP.Tool
  ( getHoverInfoFromState,
    findDefinitionRange,
    rangeFromLoc,
    posToUri,
    computeMapping,
    transformVFS,
    logWithSeverity,
    Execute,
    bindingsInRange,
    bindingsInState,
    filterByLspRange,
  )
where

import Colog.Core (LogAction, Severity, WithSeverity (WithSeverity))
import Control.Lens.Getter ((^.))
import Control.Monad.Except (ExceptT)
import Data.Foldable qualified as F
import Data.Function ((&))
import Data.Functor.Contravariant (contramap)
import Data.Map qualified as M
import Data.Text (Text)
import Data.Text qualified as T
import Futhark.Compiler.Program (lpImports)
import Futhark.LSP.PositionMapping
  ( PositionMapping,
    mappingFromDiff,
    toCurrentLoc,
    toStalePos,
  )
import Futhark.LSP.State (State (..), getStaleContent, getStaleMapping)
import Futhark.Util.Loc (Loc (Loc, NoLoc), Pos (Pos), contains, locOf)
import Futhark.Util.Pretty (prettyText)
import Language.Futhark.Core (VName, isBuiltinLoc)
import Language.Futhark.Query
  ( AtPos (AtName),
    BoundTo (..),
    allBindings,
    atPos,
    boundLoc,
    termBindingType,
  )
import Language.LSP.Logging (logToLogMessage)
import Language.LSP.Protocol.Types
  ( ErrorCodes,
    Hover (Hover),
    LSPErrorCodes,
    Location (Location),
    MarkupContent (MarkupContent),
    MarkupKind (MarkupKind_PlainText),
    Position (Position),
    Range (Range),
    UInt,
    Uri,
    filePathToUri,
    fromNormalizedFilePath,
    toNormalizedUri,
    uriToNormalizedFilePath,
    type (|?) (InL),
  )
import Language.LSP.Server (LspM, LspT, MonadLsp, getVirtualFile)
import Language.LSP.VFS (VFS, VirtualFile, vfsMap, virtualFileText, virtualFileVersion)
import Language.LSP.VFS qualified as VFS

-- | Request Handler code usually runs in this monad
type Execute a = ExceptT (Text, LSPErrorCodes |? ErrorCodes) (LspT () IO) a

-- | Retrieve hover info for the definition referenced at the given
-- file at the given line and column number (the two 'Int's).
getHoverInfoFromState :: State -> Maybe FilePath -> Int -> Int -> Maybe Hover
getHoverInfoFromState state (Just path) l c = do
  AtName _ (Just def) loc <- queryAtPos state $ Pos path l c 0
  let msg =
        case def of
          BoundTerm term _ -> prettyText $ termBindingType term
          BoundModule {} -> "module"
          BoundModuleType {} -> "module type"
          BoundType {} -> "type"
      ms = MarkupContent MarkupKind_PlainText msg
  Just $ Hover (InL ms) (Just (rangeFromLoc loc))
getHoverInfoFromState _ _ _ _ = Nothing

-- | Find the location of the definition referenced at the given file
-- at the given line and column number (the two 'Int's).
findDefinitionRange :: State -> Maybe FilePath -> Int -> Int -> Maybe Location
findDefinitionRange state (Just path) l c = do
  -- some unnessecary operations inside `queryAtPos` for this function
  -- but shouldn't affect performance much since "Go to definition" is called less frequently
  AtName _qn (Just bound) _loc <- queryAtPos state $ Pos path l c 0
  let loc = boundLoc bound
      Loc (Pos file_path _ _ _) _ = loc
  if isBuiltinLoc loc
    then Nothing
    else Just $ Location (filePathToUri file_path) (rangeFromLoc loc)
findDefinitionRange _ _ _ _ = Nothing

-- | Query the AST for information at certain Pos.
queryAtPos :: State -> Pos -> Maybe AtPos
queryAtPos state pos = do
  let Pos path _ _ _ = pos
      mapping = getStaleMapping state path
  loaded_prog <- stateProgram state
  stale_pos <- toStalePos mapping pos
  query_result <- atPos (lpImports loaded_prog) stale_pos
  updateAtPos mapping query_result pos state

-- Update the 'AtPos' with the current mapping.
updateAtPos :: Maybe PositionMapping -> AtPos -> Pos -> State -> Maybe AtPos
updateAtPos mapping (AtName qn (Just def) loc) pos state = do
  let def_loc = boundLoc def
      Loc (Pos def_file _ _ _) _ = def_loc
      Pos current_file _ _ _ = pos
  current_loc <- toCurrentLoc mapping loc
  if def_file == current_file
    then do
      current_def_loc <- toCurrentLoc mapping def_loc
      Just $ AtName qn (Just (updateBoundLoc def current_def_loc)) current_loc
    else do
      -- Defined in another file, get the corresponding PositionMapping.
      let def_mapping = getStaleMapping state def_file
      current_def_loc <- toCurrentLoc def_mapping def_loc
      Just $ AtName qn (Just (updateBoundLoc def current_def_loc)) current_loc
  where
    updateBoundLoc :: BoundTo -> Loc -> BoundTo
    updateBoundLoc (BoundTerm t _loc) current_loc = BoundTerm t current_loc
    updateBoundLoc (BoundModule _loc) current_loc = BoundModule current_loc
    updateBoundLoc (BoundModuleType _loc) current_loc = BoundModuleType current_loc
    updateBoundLoc (BoundType _loc) current_loc = BoundType current_loc
updateAtPos _ _ _ _ = Nothing

-- | Entry point for computing PositionMapping.
computeMapping :: State -> Maybe FilePath -> LspM () (Maybe PositionMapping)
computeMapping state (Just file_path) = do
  virtual_file <- getVirtualFile $ toNormalizedUri $ filePathToUri file_path
  pure $ getMapping (getStaleContent state file_path) virtual_file
  where
    getMapping :: Maybe VirtualFile -> Maybe VirtualFile -> Maybe PositionMapping
    getMapping (Just stale_file) (Just current_file) =
      if virtualFileVersion stale_file == virtualFileVersion current_file
        then Nothing -- Happens when other files (e.g. dependencies) fail to type-check.
        else Just $ mappingFromDiff (T.lines $ virtualFileText stale_file) (T.lines $ virtualFileText current_file)
    getMapping _ _ = Nothing
computeMapping _ _ = pure Nothing

-- | Convert a Futhark 'Pos' to an LSP 'Uri'.
posToUri :: Pos -> Uri
posToUri (Pos file _ _ _) = filePathToUri file

-- Futhark's parser has a slightly different notion of locations than
-- LSP; so we tweak the positions here.
getStartPos :: Pos -> Position
getStartPos (Pos _ l c _) =
  Position (toEnum l - 1) (toEnum c - 1)

getEndPos :: Pos -> Position
getEndPos (Pos _ l c _) =
  Position (toEnum l - 1) (toEnum c)

-- | Create an LSP 'Range' from a Futhark 'Loc'.
rangeFromLoc :: Loc -> Range
rangeFromLoc (Loc start end) = Range (getStartPos start) (getEndPos end)
rangeFromLoc NoLoc = Range (Position 0 0) (Position 0 5) -- only when file not found, throw error after moving to vfs

-- | Transform VFS to a map of file paths to file contents.
-- This is used to pass the file contents to the compiler.
transformVFS :: VFS -> M.Map FilePath T.Text
transformVFS vfs =
  M.foldrWithKey
    ( \uri virtual_file acc ->
        case uriToNormalizedFilePath uri of
          Nothing -> acc
          Just file_path ->
            M.insert (fromNormalizedFilePath file_path) (virtualFileText virtual_file) acc
    )
    M.empty
    (M.mapMaybe keepOpenFile (vfs ^. vfsMap))
  where
    keepOpenFile = \case
      VFS.Open file -> Just file
      VFS.Closed _ -> Nothing

logWithSeverity :: (MonadLsp c m) => Severity -> LogAction m Text
logWithSeverity severity = contramap (`WithSeverity` severity) logToLogMessage

bindingsInState :: State -> Maybe (M.Map VName BoundTo)
bindingsInState state = allBindings . lpImports <$> stateProgram state

filterByLspRange ::
  (Foldable t) =>
  Maybe PositionMapping ->
  Range ->
  FilePath ->
  (a -> Loc) ->
  t a ->
  Maybe [a]
filterByLspRange mapping range filepath f t = do
  let (Range (Position l1 c1) (Position l2 c2)) = range
  posStart <- toStalePos mapping $ mkPos l1 c1
  posEnd <- toStalePos mapping $ mkPos l2 c2
  pure $
    F.toList t
      & filter (isInRange (Loc posStart posEnd) . f)
  where
    isInRange locRange pos = case pos of
      NoLoc -> False
      Loc start _ -> locRange `contains` start
    -- increment by one: Pos counts from one onwards, LSP starts at zero
    mkPos :: UInt -> UInt -> Pos
    mkPos l c = Pos filepath (1 + fromIntegral l) (1 + fromIntegral c) (-1)

bindingsInRange :: Range -> State -> FilePath -> Maybe [(VName, BoundTo)]
bindingsInRange range state filepath = do
  let mapping = getStaleMapping state filepath
  bindings <- bindingsInState state

  bindings
    & M.assocs
    & filterByLspRange mapping range filepath (locOf . snd)