{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE ViewPatterns #-} -- Features of https://microsoft.github.io/language-server-protocol/specification.html module HsInspect.LSP.Impl where import Control.Monad (guard) import Control.Monad.Extra (fromMaybeM) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (ExceptT(..)) import Control.Monad.Trans.Except (throwE) import Data.Cache (Cache) import qualified Data.Cache as C import Data.List (nub) import Data.List.Extra (firstJust) import Data.Maybe (catMaybes, fromMaybe, listToMaybe, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Debug.Trace import qualified FastString as GHC import qualified GHC as GHC import GHC.Paths (libdir) import HsInspect.LSP.Context import HsInspect.LSP.HsInspect import qualified Lexer as GHC import qualified SrcLoc as GHC import qualified StringBuffer as GHC import System.FilePath (takeDirectory) -- TODO consider invalidation strategies, e.g. Import could use the file's -- header, and the Context use the .ghc.* files. The index might also want -- to hash the .ghc.flags contents plus any changes to exported symbols in -- the current package. But beware that often it is better to have a stale -- cache and respond with *something* than to be slow and redo the work. data Caches = Caches (Cache FilePath Context) -- ^ by source root (Cache FilePath [Import]) -- ^ by source filename (Cache FilePath [Package]) -- ^ by source root -- TODO the index could use a data structure that is faster to search cachedContext :: Caches -> FilePath -> ExceptT String IO Context cachedContext (Caches cache _ _) file = do key <- takeDirectory <$> discoverGhcflags file let work = do ctx <- findContext file liftIO $ C.insert cache key ctx pure ctx fromMaybeM work . liftIO $ C.lookup cache key cachedImports :: Caches -> Context -> FilePath -> ExceptT String IO [Import] cachedImports (Caches _ cache _) ctx file = C.fetchWithCache cache file $ hsinspect_imports ctx cachedIndex :: Caches -> Context -> ExceptT String IO [Package] cachedIndex (Caches _ _ cache) ctx = C.fetchWithCache cache (srcdir ctx) $ \_ -> hsinspect_index ctx -- only lookup the index, don't try to populate it cachedIndex' :: Caches -> Context -> ExceptT String IO [Package] cachedIndex' (Caches _ _ cache) ctx = liftIO $ fromMaybe [] <$> C.lookup cache (srcdir ctx) -- zero indexed data Span = Span Int Int Int Int -- line, col, line, col deriving Show getFullModuleName :: Text -> [Import] -> Maybe Text getFullModuleName query imports = case results of [] -> Nothing result:_ -> Just result where results = mapMaybe extractModule imports extractModule imp = do qualName <- _qual imp guard $ T.isPrefixOf query qualName let fullImport = _full imp let (T.init -> module', _) = T.breakOnEnd "." fullImport return $ module' findType :: Text -> [Package] -> Maybe Text findType qual pkgs = listToMaybe $ do let (T.init -> module', sym) = T.breakOnEnd "." qual let flatten Nothing = [] flatten (Just as) = as pkg <- pkgs Module module'' entries <- flatten . _modules $ pkg if module'' /= module' then [] else do e <- flatten entries let matcher name typ = if name == sym && name /= typ then [typ] else [] case e of Id _ name typ -> matcher name typ Con _ name typ -> matcher name typ Pat _ name typ -> matcher name typ TyCon _ _ _ -> [] findNameAndTypes :: Text -> [Package] -> [(Text, Text, Text)] findNameAndTypes qual pkgs = do let (_, sym) = T.breakOnEnd "." qual let flatten Nothing = [] flatten (Just as) = as pkg <- pkgs Module m entries <- flatten . _modules $ pkg e <- flatten entries let matcher name typ = if T.isPrefixOf sym name then [(name, typ, m)] else [] case traceShowId e of Id _ name typ -> matcher name typ Con _ name typ -> matcher name typ Pat _ name typ -> matcher name typ TyCon _ _ _ -> [] hoverProvider :: Caches -> FilePath -> (Int, Int) -> ExceptT String IO (Maybe (Span, Text)) hoverProvider caches file position = do ctx <- cachedContext caches file symbols <- cachedImports caches ctx file index <- cachedIndex' caches ctx found <- symbolAtPoint file position pure $ case traceShow found found of Nothing -> Nothing Just (range, sym) -> let matcher imp = if _local imp == Just sym || _qual imp == Just sym || _full imp == sym then Just $ case findType (_full imp) index of Just typ -> _full imp <> " :: " <> typ Nothing -> _full imp else Nothing in (range,) <$> firstJust matcher symbols -- TODO use the index to add optional type information completionProvider :: Caches -> FilePath -> Text -> (Int, Int) -> ExceptT String IO [(Text, Maybe Text)] completionProvider caches file contents position = do ctx <- cachedContext caches file symbols <- cachedImports caches ctx file index <- cachedIndex' caches ctx found <- symbolAtVirtualPoint file contents position pure $ case traceShow (found, symbols) found of Nothing -> [] Just (_, sym) -> do let findNameAndType :: Text -> Import -> (Text, Maybe Text) findNameAndType name imp = case findType (_full imp) index of Just typ -> (name, Just typ) Nothing -> (name, Nothing) matcher :: (Import -> Maybe Text) -> Import -> Maybe (Text, Maybe Text) matcher key imp = do pref <- key imp if T.isPrefixOf sym pref then return $ findNameAndType pref imp else Nothing let localMatches = map (matcher _local) symbols let qualMatches = map (matcher _qual) symbols let fullMatches = map (matcher (Just . _full)) symbols nub $ catMaybes $ localMatches <> qualMatches <> fullMatches -- c.f. haskell-tng--hsinspect-symbol-at-point -- -- TODO consider replacing this (inefficient) ghc api usage with a regexp or -- calling the specific lexer functions directly for the symbols we support. symbolAtPoint :: FilePath -> (Int, Int) -> ExceptT String IO (Maybe (Span, Text)) symbolAtPoint file (line, col) = do buf' <- liftIO $ GHC.hGetStringBuffer file -- TODO for performance, and language extension reliability, find out how to -- start the lexer on the correct line, to avoid lexing stuff we don't -- care about. buf <- maybe (throwE "line doesn't exist") pure $ GHC.atLine 1 buf' let file' = GHC.mkFastString file startLoc = GHC.mkRealSrcLoc file' 1 1 -- add 1 to the column because GHC.containsSpan doesn't seem to like it -- when the point is on the very first character of the span and if we -- must pick between the first or last char, we prefer the first. point = GHC.realSrcLocSpan $ GHC.mkRealSrcLoc file' line (col + 1) -- TODO construct the real dflags from the Context (remove libdir dependency) dflags <- liftIO . GHC.runGhc (Just libdir) $ GHC.getSessionDynFlags -- lexTokenStream :: StringBuffer -> RealSrcLoc -> DynFlags -> ParseResult [Located Token] case GHC.lexTokenStream buf startLoc dflags of GHC.POk _ ts -> let containsPoint :: (GHC.Located GHC.Token, String) -> Maybe (Span, Text) containsPoint ((GHC.L (GHC.UnhelpfulSpan _) _), _) = Nothing containsPoint ((GHC.L (GHC.RealSrcSpan s) _), txt) = if GHC.containsSpan s point then Just (toSpan s, T.pack txt) else Nothing toSpan src = Span (GHC.srcSpanStartLine src - 1) (GHC.srcSpanStartCol src - 1) (GHC.srcSpanEndLine src - 1) (GHC.srcSpanEndCol src - 1) in pure . firstJust containsPoint $ GHC.addSourceToTokens startLoc buf ts _ -> throwE "lexer error" -- TODO getErrorMessages symbolAtVirtualPoint :: FilePath -> Text -> (Int, Int) -> ExceptT String IO (Maybe (Span, Text)) symbolAtVirtualPoint file contents (line, col) = do let buf' = GHC.stringToStringBuffer $ T.unpack contents buf <- maybe (throwE "line doesn't exist") pure $ GHC.atLine 1 buf' let file' = GHC.mkFastString file startLoc = GHC.mkRealSrcLoc file' 1 1 -- when you autocomplete you're at the end of word -- you do not want suggestions for the next column point = GHC.realSrcLocSpan $ GHC.mkRealSrcLoc file' line col -- TODO construct the real dflags from the Context (remove libdir dependency) dflags <- liftIO . GHC.runGhc (Just libdir) $ GHC.getSessionDynFlags -- lexTokenStream :: StringBuffer -> RealSrcLoc -> DynFlags -> ParseResult [Located Token] case GHC.lexTokenStream buf startLoc dflags of GHC.POk _ ts -> let containsPoint :: (GHC.Located GHC.Token, String) -> Maybe (Span, Text) containsPoint ((GHC.L (GHC.UnhelpfulSpan _) _), _) = Nothing containsPoint ((GHC.L (GHC.RealSrcSpan s) _), txt) = if GHC.containsSpan s point then Just (toSpan s, T.pack txt) else Nothing toSpan src = Span (GHC.srcSpanStartLine src - 1) (GHC.srcSpanStartCol src - 1) (GHC.srcSpanEndLine src - 1) (GHC.srcSpanEndCol src - 1) in pure . firstJust containsPoint $ GHC.addSourceToTokens startLoc buf ts _ -> throwE "lexer error" -- TODO getErrorMessages