glean-lsp (empty) → 0.1.0.0
raw patch · 7 files changed
+1223/−0 lines, 7 filesdep +aesondep +basedep +containers
Dependencies added: aeson, base, containers, data-default, directory, exceptions, fb-util, filepath, glean, hashable, lsp, optparse-applicative, text, thrift-lib, transformers, unliftio, unliftio-core, unordered-containers
Files
- CHANGELOG.md +5/−0
- Data/ConcurrentCache.hs +72/−0
- Data/Path.hs +110/−0
- Glean/LSP.hs +694/−0
- LICENSE +70/−0
- README.md +179/−0
- glean-lsp.cabal +93/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for glean-lsp++## 0.1.0.0++* First version. Released on an unsuspecting world.
+ Data/ConcurrentCache.hs view
@@ -0,0 +1,72 @@+{-+ Derived from Data.ConcurrentCache in static-ls++ Copyright (c) 2023 Joseph Sumabat+-}++module Data.ConcurrentCache (+ ConcurrentCache,+ new,+ flush,+ remove,+ insert+) where++import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Data.Hashable+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import UnliftIO.Exception+import UnliftIO.IORef+import UnliftIO.MVar++newtype ConcurrentCache k v = ConcurrentCache {+ map :: IORef (HashMap k (MVar (Maybe v)))+ -- k -> missing => computation for k not cached+ -- k -> MVar empty => computation for k in progress+ -- k -> MVar (Just v) => computation for k cached with value v+ -- k -> MVar Nothing => computation for k failed, try again+}++new :: (MonadIO m) => m (ConcurrentCache k v)+new = do+ map <- newIORef HashMap.empty+ pure ConcurrentCache {map}++flush :: (MonadIO m) => ConcurrentCache k v -> m ()+flush cache = writeIORef cache.map HashMap.empty++remove :: (Hashable k, MonadIO m) => k -> ConcurrentCache k v -> m ()+remove k cache = do+ atomicModifyIORef' cache.map $ \m -> do+ (HashMap.delete k m, ())++-- | Perform a computation and insert its result into the cache. If+-- multiple threads try to insert a computation for the same key, one+-- will perform its computation while the others wait. If the+-- computation throws, another thread will try its computation.+insert :: (Hashable k, MonadUnliftIO m) => k -> ConcurrentCache k v -> m v -> m v+insert k cache act = mask $ \restore -> do+ var <- newEmptyMVar+ existed <- atomicModifyIORef' cache.map $ \m -> do+ case HashMap.lookup k m of+ Nothing -> (HashMap.insert k var m, Nothing)+ Just otherVar -> (m, Just otherVar)+ case existed of+ Nothing -> do+ v <-+ (restore act) `onException` do+ remove k cache+ putMVar var Nothing+ putMVar var (Just v)+ pure v+ Just otherVar ->+ join $ restore $ do+ v <- readMVar otherVar+ case v of+ Just v -> return $ return v+ Nothing ->+ -- try again; the key was removed from the cache+ return $ insert k cache act
+ Data/Path.hs view
@@ -0,0 +1,110 @@+{-+ Copyright (c) 2023 Joseph Sumabat++ See glean/lsp/LICENSE+-}++module Data.Path (+ Path (path, Path, UncheckedPath),+ KnownPathKind (sPathKind),+ AbsPath,+ PathKind (..),+ RelPath,+ filePathToAbs,+ unsafeFilePathToAbs,+ filePathToRel,+ toFilePath,+ (</>),+ makeRelative,+ absToRel,+ (<.>),+ (-<.>),+ filePathToAbsThrow,+ uncheckedCoercePath,+ relToAbsThrow,+)+where++import Control.Monad.Catch+import Control.Monad.IO.Class+import Data.Aeson qualified as Aeson+import Data.Coerce (coerce)+import Data.Hashable (Hashable)+import Data.String (IsString (..))+import GHC.Stack (HasCallStack)+import System.Directory qualified as Dir+import System.FilePath qualified as FilePath+import UnliftIO (stringException)++-- | Rel means may be relative or absolute, absolute means must be absolute+data PathKind = Rel | Abs++data SPathKind p where+ SRel :: SPathKind Rel+ SAbs :: SPathKind Abs++class KnownPathKind p where+ sPathKind :: SPathKind p++instance KnownPathKind Abs where+ sPathKind = SAbs++instance KnownPathKind Rel where+ sPathKind = SRel++newtype Path (p :: PathKind) = UncheckedPath {path :: FilePath}+ deriving (Show, Eq, Ord, Hashable, Aeson.FromJSON, Aeson.ToJSON)++uncheckedCoercePath :: Path p -> Path q+uncheckedCoercePath = coerce++instance IsString (Path Rel) where+ fromString = UncheckedPath++pattern Path :: FilePath -> Path p+pattern Path p <- UncheckedPath p++type AbsPath = Path Abs++type RelPath = Path Rel++toFilePath :: (HasCallStack) => Path p -> FilePath+toFilePath = (.path)++filePathToRel :: (HasCallStack) => FilePath -> RelPath+filePathToRel = UncheckedPath++filePathToAbs :: (HasCallStack, MonadIO m) => FilePath -> m AbsPath+filePathToAbs p = do+ absPath <- liftIO $ Dir.makeAbsolute p+ pure $ UncheckedPath absPath++unsafeFilePathToAbs :: (HasCallStack) => FilePath -> AbsPath+unsafeFilePathToAbs p+ | FilePath.isAbsolute p = UncheckedPath p+ | otherwise = error "unsafeOsPathToAbs: path is not absolute"++relToAbsThrow :: (MonadThrow m, HasCallStack) => RelPath -> m AbsPath+relToAbsThrow (UncheckedPath p) = filePathToAbsThrow p++filePathToAbsThrow :: (MonadThrow m, HasCallStack) => FilePath -> m AbsPath+filePathToAbsThrow p+ | FilePath.isAbsolute p = pure $ UncheckedPath p+ | otherwise = throwM (stringException $ "filepath was not absolute: " ++ p)++(</>) :: Path p -> Path Rel -> Path p+(UncheckedPath p) </> (UncheckedPath p') = UncheckedPath (p FilePath.</> p')++infixr 5 </>++(<.>) :: Path p -> String -> Path p+(UncheckedPath p) <.> ext = UncheckedPath (p FilePath.<.> ext)++(-<.>) :: Path p -> String -> Path p+(UncheckedPath p) -<.> ext = UncheckedPath (p FilePath.-<.> ext)++absToRel :: AbsPath -> RelPath+absToRel (UncheckedPath p) = UncheckedPath p++makeRelative :: Path p -> Path q -> Path Rel+makeRelative (UncheckedPath p) (UncheckedPath q) = UncheckedPath (FilePath.makeRelative p q)
+ Glean/LSP.hs view
@@ -0,0 +1,694 @@+{-# LANGUAGE ApplicativeDo, RecursiveDo #-}+module Glean.LSP (main) where++import Control.Monad+import Control.Monad.Catch+import Control.Monad.Fix+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Control.Monad.Trans.Class+import Control.Monad.Trans.Except+import Control.Monad.Trans.Reader+import Data.Aeson as Aeson+import Data.Default+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Int+import Data.List (sortBy)+import qualified Data.Map as Map+import Data.Maybe+import Data.Ord (comparing)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Language.LSP.Server as LSP+import Language.LSP.Server (LspM)+import qualified Language.LSP.Protocol.Message as LSP+import qualified Language.LSP.Protocol.Types as LSP+import Options.Applicative+import Thrift.Protocol+import UnliftIO.Async+import UnliftIO.Exception+import UnliftIO.IORef+import Util.Log.Text+import Util.Timing++-- use Glass as a library directly+import qualified Glean.Glass.Env as Glass+import qualified Glean.Glass.Main as Glass+import qualified Glean.Glass.Options as Glass+import qualified Glean.Glass.Tracing as Glass+import qualified Glean.Glass.Types as Glass+import qualified Glean.Glass.Range as Glass+import qualified Glean.Glass.Handler.Documents as Glass.Handler+import qualified Glean.Glass.Handler.Symbols as Glass.Handler++import Data.ConcurrentCache as ConcurrentCache+import Data.Path as Path++{- TODO / ideas+ - go to decl / go to impl / go to type def?+ - call hierarchy+ - documentSymbols:+ - maybe filter out type parameters?+ - update index after edits+ 1. make it possible to re-index on the side and create a new DB,+ using a separate glean-server. Need to flush the cache if we+ detect that the data has changed.+ 2. Trigger re-indexing from the UI somehow, with a progress update+ 3. Support full re-index & in-memory incremental index for changed files only?+ - can we show errors somehow?+ - maybe use tree-sitter to get uptodate documentSymbols and+ merge those with the real symbols from Glass?+ - implement setTrace?+-}++-- -----------------------------------------------------------------------------+-- Command-line options++-- | Inherit CLI options from Glass+data LspOptions = LspOptions {+ glass :: Glass.Config Glass.GlassTrace+ }++options :: ParserInfo LspOptions+options = info (helper <*> (LspOptions <$> Glass.configParser)) fullDesc++-- -----------------------------------------------------------------------------+-- Config++-- | Config from the LSP client. e.g. for VS Code, @<workspace>/.vscode/settings.json@:+--+-- > {+-- > "glean-lsp": {+-- > "repo": "stackage"+-- > },+-- > ...+-- > }+--+data LspConfig = LspConfig {+ repo :: Glass.RepoName+ }+ deriving (Show)++instance FromJSON LspConfig where+ parseJSON = withObject "LspConfig" $ \v -> LspConfig+ <$> fmap Glass.RepoName (v .: "repo")++instance Default LspConfig where+ def = LspConfig {+ repo = Glass.RepoName "stackage"+ }++-- -----------------------------------------------------------------------------+-- Monad++-- | Environment of the running LSP server after initialisation+data LspEnv = LspEnv {+ options :: LspOptions,+ wsRoot :: AbsPath,+ glass :: Glass.Env,+ symbolCache :: ConcurrentCache RelPath Glass.DocumentSymbolIndex,+ requests :: IORef (HashMap LSP.SomeLspId (Async ()))+ }++newtype GleanLspM a =+ GleanLspM { unGleanLspM :: ReaderT (IORef (Maybe LspEnv)) (LspM LspConfig) a }+ deriving (Monad, Applicative, Functor, MonadIO, MonadThrow, MonadUnliftIO, MonadFix)++deriving instance LSP.MonadLsp LspConfig GleanLspM++runGleanLspM :: IORef (Maybe LspEnv) -> GleanLspM a -> LspM LspConfig a+runGleanLspM envRef = flip runReaderT envRef . (.unGleanLspM)++newtype GleanLspException = GleanLspException Text+ deriving Show++instance Exception GleanLspException++getGleanLspEnv :: GleanLspM LspEnv+getGleanLspEnv = GleanLspM $ do+ ref <- ask+ r <- readIORef ref+ case r of+ Nothing -> throwIO $ GleanLspException "not initialized"+ Just env -> return env++-- -----------------------------------------------------------------------------+-- Executing requests++-- | Perform a request asynchronously. It can subsequently be+-- cancelled by 'cancelRequest', which will send the appropriate+-- @RequestCancelled@ response back to the client if the request was+-- still in progress at the time of cancellation.+asyncRequest ::+ forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .+ (LSP.TRequestMessage m ->+ GleanLspM (Either (LSP.TResponseError m) (LSP.MessageResult m))) ->+ LSP.Handler GleanLspM m+asyncRequest act = \msg respond -> mdo+ env <- getGleanLspEnv+ m <- readIORef env.requests+ liftIO $ logInfo $ "asyncRequest: " <>+ Text.pack (show (HashMap.size m)) <> " in progress"+ let+ register =+ atomicModifyIORef env.requests $ -- not strict due to mdo+ \m -> (HashMap.insert (LSP.SomeLspId msg._id) async m, ())+ unregister =+ atomicModifyIORef' env.requests $+ \m -> (HashMap.delete (LSP.SomeLspId msg._id) m, ())+ register+ async <- UnliftIO.Exception.mask_ $ asyncWithUnmask $ \unmask -> do+ r <- UnliftIO.Exception.trySyncOrAsync $ unmask $ act msg+ -- we want to catch everything, including async cancellation+ unregister+ case r of+ Left (ex :: SomeException)+ | Just AsyncCancelled <- fromException ex ->+ respond $ Left $ responseCancelled "cancelled by client"+ | otherwise -> respond $ Left $ responseException ex+ Right result -> respond result+ return ()++-- | Cancel a request by LspId+cancelRequest ::+ forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .+ LSP.LspId m ->+ GleanLspM ()+cancelRequest id = do+ env <- getGleanLspEnv+ m <- readIORef env.requests+ case HashMap.lookup (LSP.SomeLspId id) m of+ Nothing -> logWarning $ "cancelRequest: not found"+ Just async -> cancel async++responseCancelled ::+ forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .+ Text ->+ LSP.TResponseError m+responseCancelled msg =+ LSP.TResponseError {+ _code = LSP.InL LSP.LSPErrorCodes_RequestCancelled,+ _message = msg,+ _xdata = Nothing+ }++responseException ::+ forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .+ SomeException ->+ LSP.TResponseError m+responseException ex =+ LSP.TResponseError {+ _code = LSP.InL LSP.LSPErrorCodes_RequestFailed,+ _message = Text.pack (show ex),+ _xdata = Nothing+ }++-- -----------------------------------------------------------------------------+-- Setup & initialisation++initServer ::+ Glass.Env ->+ LspOptions ->+ IORef (Maybe LspEnv) ->+ LSP.LanguageContextEnv LspConfig ->+ LSP.TMessage 'LSP.Method_Initialize ->+ IO (Either (LSP.TResponseError LSP.Method_Initialize) (LSP.LanguageContextEnv LspConfig))+initServer glass options envRef serverConfig _msg = do+ runExceptT $ do+ wsRoot <- ExceptT $ LSP.runLspT serverConfig getWsRoot+ wsRoot <- filePathToAbs wsRoot+ symbolCache <- ConcurrentCache.new+ requests <- newIORef HashMap.empty+ writeIORef envRef (Just LspEnv { options, glass, wsRoot, symbolCache, requests })+ liftIO $ logInfo $ "wsRoot: " <> Text.pack (Path.toFilePath wsRoot)+ pure serverConfig+ where+ getWsRoot :: LSP.LspM config (Either (LSP.TResponseError LSP.Method_Initialize) FilePath)+ getWsRoot = do+ mRootPath <- LSP.getRootPath+ pure $ case mRootPath of+ Nothing -> Left $ LSP.TResponseError (LSP.InR LSP.ErrorCodes_InvalidRequest)+ "No root workspace was found" Nothing+ Just p -> Right p++serverDef+ :: Glass.Env+ -> LspOptions+ -> IO (LSP.ServerDefinition LspConfig)+serverDef glass options = do+ envRef <- newIORef Nothing+ let+ mapReq ::+ forall (a :: LSP.Method LSP.ClientToServer LSP.Request).+ LSP.Handler GleanLspM a ->+ LSP.Handler (LspM LspConfig) a+ mapReq f = \msg responseCont -> runGleanLspM envRef $+ f msg (\resp -> GleanLspM (lift (responseCont resp)))++ mapNot ::+ forall (a :: LSP.Method LSP.ClientToServer LSP.Notification).+ LSP.Handler GleanLspM a ->+ LSP.Handler (LspM LspConfig) a+ mapNot f = \msg -> runGleanLspM envRef $ f msg++ pure+ LSP.ServerDefinition+ { onConfigChange = \_conf -> runGleanLspM envRef $ do+ liftIO $ logInfo "config change"+ flushSymbolCache+ , configSection = "glean-lsp"+ , parseConfig = \_conf value ->+ case fromJSON value of+ Error err -> Left (Text.pack err)+ Aeson.Success conf -> Right conf+ , doInitialize = initServer glass options envRef+ , -- TODO: Do handlers need to inspect clientCapabilities?+ staticHandlers = \_clientCapabilities ->+ LSP.mapHandlers mapReq mapNot $+ mconcat [+ handleInitialized+ , handleChangeConfiguration+ , handleTextDocumentHoverRequest+ , handleDefinitionRequest+ -- , handleTypeDefinitionRequest+ -- , handleImplementationRequest+ , handleReferencesRequest+ -- , handleRenameRequest+ -- , handlePrepareRenameRequest+ , handleCancelNotification+ , handleDidOpen+ -- , handleDidChange+ -- , handleDidSave+ , handleDidClose+ , handleWorkspaceSymbol+ , handleSetTrace+ -- , handleCodeAction+ -- , handleResolveCodeAction+ , handleDocumentSymbols+ -- , handleCompletion+ -- , handleCompletionItemResolve+ ]+ , interpretHandler = \env -> LSP.Iso (LSP.runLspT env) liftIO+ , options = lspOptions+ , defaultConfig = def+ }++lspOptions :: LSP.Options+lspOptions =+ LSP.defaultOptions+ { LSP.optTextDocumentSync =+ Just+ LSP.TextDocumentSyncOptions+ { LSP._openClose = Just True+ , LSP._change = Just LSP.TextDocumentSyncKind_Incremental+ , LSP._willSave = Just False+ , LSP._willSaveWaitUntil = Just False+ , LSP._save =+ Just $+ LSP.InR $+ LSP.SaveOptions+ { LSP._includeText = Just False+ }+ }+ , LSP.optCompletionTriggerCharacters = Just ['.']+ }++main :: IO ()+main = do+ opts <- execParser options+ Glass.withEnv opts.glass Nothing $ \glass -> do+ server <- serverDef glass opts+ void $ LSP.runServer server++-- -----------------------------------------------------------------------------+-- Handlers++handleChangeConfiguration :: LSP.Handlers GleanLspM+handleChangeConfiguration =+ LSP.notificationHandler LSP.SMethod_WorkspaceDidChangeConfiguration $+ pure $ pure ()++handleInitialized :: LSP.Handlers GleanLspM+handleInitialized =+ LSP.notificationHandler LSP.SMethod_Initialized $+ pure $ pure ()++handleCancelNotification :: LSP.Handlers GleanLspM+handleCancelNotification =+ LSP.notificationHandler LSP.SMethod_CancelRequest $ \req -> do+ let id = case req._params._id of+ LSP.InL i -> LSP.IdInt i+ LSP.InR t -> LSP.IdString t+ liftIO $ logInfo $ "cancel: " <> Text.pack (show id)+ cancelRequest id++handleDidOpen :: LSP.Handlers GleanLspM+handleDidOpen =+ LSP.notificationHandler LSP.SMethod_TextDocumentDidOpen $ \message -> do+ liftIO $ logInfo $ "did open: " <> message._params._textDocument._uri.getUri++handleDidClose :: LSP.Handlers GleanLspM+handleDidClose =+ LSP.notificationHandler LSP.SMethod_TextDocumentDidClose $ \req -> do+ let uri = req._params._textDocument._uri+ liftIO $ logInfo $ "did close: " <> uri.getUri+ path <- uriToAbsPath uri+ removeCachedSymbols path++handleDocumentSymbols :: LSP.Handlers GleanLspM+handleDocumentSymbols =+ LSP.requestHandler LSP.SMethod_TextDocumentDocumentSymbol $ asyncRequest $ \req ->+ logTimed ("documentSymbols: " <> req._params._textDocument._uri.getUri) $ do+ let params = req._params+ path <- uriToAbsPath params._textDocument._uri+ syms <- getDocumentSymbols path+ liftIO $ logInfo $ "symbols: " <> Text.pack (show (length syms))+ return $ Right $ LSP.InR $ LSP.InL syms++handleDefinitionRequest :: LSP.Handlers GleanLspM+handleDefinitionRequest =+ LSP.requestHandler LSP.SMethod_TextDocumentDefinition $ asyncRequest $ \req -> do+ let params = req._params+ logTimed ("definition: " <> params._textDocument._uri.getUri <>+ Text.pack (show req._params._position)) $ do+ path <- uriToAbsPath params._textDocument._uri+ defs <- getDefinition path params._position+ return $ Right . LSP.InR $ LSP.InL defs++handleSetTrace :: LSP.Handlers GleanLspM+handleSetTrace = LSP.notificationHandler LSP.SMethod_SetTrace $ \_ -> pure ()++handleTextDocumentHoverRequest :: LSP.Handlers GleanLspM+handleTextDocumentHoverRequest =+ LSP.requestHandler LSP.SMethod_TextDocumentHover $ asyncRequest $ \req -> do+ let hoverParams = req._params+ logTimed ("hover: " <> hoverParams._textDocument._uri.getUri <>+ Text.pack (show hoverParams._position)) $ do+ path <- uriToAbsPath hoverParams._textDocument._uri+ hover <- retrieveHover path hoverParams._position+ return $ Right $ LSP.maybeToNull hover++handleReferencesRequest :: LSP.Handlers GleanLspM+handleReferencesRequest =+ LSP.requestHandler LSP.SMethod_TextDocumentReferences $ asyncRequest $ \req -> do+ let params = req._params+ logTimed ("references: " <> params._textDocument._uri.getUri <>+ Text.pack (show params._position)) $ do+ path <- uriToAbsPath params._textDocument._uri+ refs <- findRefs path params._position+ return $ Right $ LSP.InL refs++handleWorkspaceSymbol :: LSP.Handlers GleanLspM+handleWorkspaceSymbol =+ LSP.requestHandler LSP.SMethod_WorkspaceSymbol $ asyncRequest $ \req ->+ logTimed ("search: " <> req._params._query) $ do+ symbols <- symbolSearch req._params._query+ return $ Right . LSP.InL $ symbols++-- -----------------------------------------------------------------------------+-- Glean / Glass stuff++getDefinition ::+ AbsPath ->+ LSP.Position ->+ GleanLspM [LSP.DefinitionLink]+getDefinition path lineCol = do+ env <- getGleanLspEnv+ symbols <- findSymbol lineCol <$> getSymbolsCached path+ return $ fmap (LSP.DefinitionLink . locationToLocationLink) $+ refTargets env.wsRoot symbols++getSymbols ::+ RelPath ->+ Bool ->+ GleanLspM Glass.DocumentSymbolIndex+getSymbols path includeRefs = do+ env <- getGleanLspEnv+ cfg :: LspConfig <- LSP.getConfig+ let+ query = def {+ Glass.documentSymbolsRequest_repository = cfg.repo+ , Glass.documentSymbolsRequest_filepath =+ Glass.Path (Text.pack (Path.toFilePath path))+ , Glass.documentSymbolsRequest_include_refs = includeRefs+ }+ opts = def+ liftIO $ Glass.Handler.documentSymbolIndex env.glass query opts++getSymbolsCached :: AbsPath -> GleanLspM Glass.DocumentSymbolIndex+getSymbolsCached path = do+ env <- getGleanLspEnv+ let relPath = Path.makeRelative env.wsRoot path+ ConcurrentCache.insert relPath env.symbolCache $+ getSymbols relPath True {- includeRefs -}++flushSymbolCache :: GleanLspM ()+flushSymbolCache = do+ env <- getGleanLspEnv+ ConcurrentCache.flush env.symbolCache++removeCachedSymbols :: AbsPath -> GleanLspM ()+removeCachedSymbols path = do+ env <- getGleanLspEnv+ let relPath = Path.makeRelative env.wsRoot path+ ConcurrentCache.remove relPath env.symbolCache++findSymbol ::+ LSP.Position ->+ Glass.DocumentSymbolIndex ->+ [Glass.SymbolX]+findSymbol (LSP.Position l c) ix =+ [ sym+ | sym <- Map.findWithDefault [] (fromIntegral (l+1)) refs+ , inRange (fromIntegral (l+1)) (fromIntegral (c+1)) sym.symbolX_range+ ]+ where+ refs = Glass.documentSymbolIndex_symbols ix+ inRange line col (Glass.Range lb cb le ce) =+ line >= lb && line <= le &&+ (if line == lb then col >= cb else True) &&+ (if line == le then col <= ce else True)++retrieveHover ::+ AbsPath ->+ LSP.Position ->+ GleanLspM (Maybe LSP.Hover)+retrieveHover path position = do+ syms <- getSymbolsCached path+ case findSymbol position syms of+ (sym:_) | Just ty <- attrSymbolSignature sym.symbolX_attributes -> do+ -- TODO: pick the innermost match+ logInfo $ "hover: " <> Text.pack (show sym)+ return $ Just $ LSP.Hover+ { _range = Just $ toLspRange sym.symbolX_range+ , _contents = LSP.InL $ LSP.MarkupContent LSP.MarkupKind_PlainText ty+ }+ _ -> return Nothing++findRefs ::+ AbsPath ->+ LSP.Position ->+ GleanLspM [LSP.Location]+findRefs path pos = do+ logInfo $ "Glean.findRefs: " <> Text.pack (show pos)+ env <- getGleanLspEnv+ syms <- getSymbolsCached path+ case findSymbol pos syms of+ [] -> do+ logInfo $ "not found"+ return []+ (defn:_) -> do+ -- TODO: pick the innermost or tightest range if there are many+ logInfo $ "found: " <> Text.pack (show defn)+ ranges <- liftIO $+ Glass.Handler.findReferenceRanges env.glass defn.symbolX_sym def+ return (map (toLspLocation env.wsRoot) ranges)++-- | Document symbols, used to generate the outline.+--+-- We want a hierarchical symbol structure for the outline. Glass+-- provides a symbolParent attribute, but this only reflects the+-- parent container of the qualified name which is usually the module+-- or namespace, not the full containment relation.+--+-- We could ask Glass for the containment relation, but that would+-- likely be expensive. So instead we reconstruct an approximate+-- symbol hierarchy using source ranges - arguably this is what we+-- want for the outline anyway.+--+getDocumentSymbols ::+ AbsPath ->+ GleanLspM [LSP.DocumentSymbol]+getDocumentSymbols path = do+ syms <- getSymbolsCached path+ let+ defs =+ [ sym+ | sym <- concat $ Map.elems syms.documentSymbolIndex_symbols+ , isNothing sym.symbolX_target -- only definitions+ ]+ return $ mkSymbolTree (sortBy (comparing (.symbolX_range)) defs)+ where+ mkSymbolTree [] = []+ mkSymbolTree (sym : rest)+ | Just name <- attrSymbolName sym.symbolX_attributes =+ parent name : mkSymbolTree others+ | otherwise = mkSymbolTree rest+ where+ (children, others) = span isChild rest+ isChild child = sym.symbolX_range `Glass.rangeContains` child.symbolX_range+ parent name = LSP.DocumentSymbol {+ _name = name,+ _detail = attrSymbolSignature sym.symbolX_attributes,+ _kind = kind,+ _tags = Nothing, -- TODO?+ _deprecated = Nothing,+ _range = range,+ _selectionRange = range,+ _children = case mkSymbolTree children of+ [] -> Nothing+ some -> Just some+ }+ kind = fromMaybe LSP.SymbolKind_Function (attrSymbolKind sym.symbolX_attributes)+ range = toLspRange sym.symbolX_range++symbolSearch ::+ Text ->+ GleanLspM [LSP.SymbolInformation]+symbolSearch query = do+ env <- getGleanLspEnv+ cfg :: LspConfig <- LSP.getConfig+ let+ req = def {+ Glass.symbolSearchRequest_name = query,+ Glass.symbolSearchRequest_repo_name = Just cfg.repo,+ Glass.symbolSearchRequest_options = def {+ Glass.symbolSearchOptions_detailedResults = True -- we need kinds+ }+ }+ opts = def+ res <- liftIO $ Glass.Handler.searchSymbol env.glass req opts+ return [+ LSP.SymbolInformation {+ _name = sym.symbolDescription_name.qualifiedName_localName.unName,+ _kind = maybe LSP.SymbolKind_Function toLspSymbolKind sym.symbolDescription_kind,+ _tags = Nothing,+ _deprecated = Nothing,+ _location = toLspLocation env.wsRoot sym.symbolDescription_sym_location,+ _containerName = Nothing+ }+ | sym <- res.symbolSearchResult_symbolDetails+ ]++-- -----------------------------------------------------------------------------+-- Data conversion Glass <-> LSP++refTargets :: AbsPath -> [Glass.SymbolX] -> [LSP.Location]+refTargets wsRoot syms =+ [ toLspLocation wsRoot rg+ | Glass.SymbolX{symbolX_target = Just rg} <- syms+ ]++toLspLocation :: AbsPath -> Glass.LocationRange -> LSP.Location+toLspLocation wsRoot locRange =+ LSP.Location uri (toLspRange locRange.locationRange_range)+ where+ path = Glass.unPath locRange.locationRange_filepath+ uri = absPathToUri (wsRoot Path.</> Path.filePathToRel (Text.unpack path))++toLspPosition :: Int64 -> Int64 -> LSP.Position+toLspPosition line col =+ LSP.Position (fromIntegral (line-1)) (fromIntegral (col-1))++toLspRange :: Glass.Range -> LSP.Range+toLspRange range = LSP.Range begin end+ where+ begin = toLspPosition range.range_lineBegin range.range_columnBegin+ end = toLspPosition range.range_lineEnd range.range_columnEnd++toLspSymbolKind :: Glass.SymbolKind -> LSP.SymbolKind+toLspSymbolKind = \case+ Glass.SymbolKind_Package -> LSP.SymbolKind_Package+ Glass.SymbolKind_Type -> LSP.SymbolKind_Class -- ?+ Glass.SymbolKind_Value -> LSP.SymbolKind_Constant -- ?+ Glass.SymbolKind_File -> LSP.SymbolKind_File+ Glass.SymbolKind_Module -> LSP.SymbolKind_Module+ Glass.SymbolKind_Namespace -> LSP.SymbolKind_Namespace+ Glass.SymbolKind_Class_ -> LSP.SymbolKind_Class+ Glass.SymbolKind_Method -> LSP.SymbolKind_Method+ Glass.SymbolKind_Property -> LSP.SymbolKind_Property+ Glass.SymbolKind_Field -> LSP.SymbolKind_Field+ Glass.SymbolKind_Constructor -> LSP.SymbolKind_Constructor+ Glass.SymbolKind_Enum -> LSP.SymbolKind_Enum+ Glass.SymbolKind_Interface -> LSP.SymbolKind_Interface+ Glass.SymbolKind_Function -> LSP.SymbolKind_Function+ Glass.SymbolKind_Variable -> LSP.SymbolKind_Variable+ Glass.SymbolKind_Constant -> LSP.SymbolKind_Constant+ Glass.SymbolKind_String -> LSP.SymbolKind_String+ Glass.SymbolKind_Number -> LSP.SymbolKind_Number+ Glass.SymbolKind_Boolean -> LSP.SymbolKind_Boolean+ Glass.SymbolKind_Array -> LSP.SymbolKind_Array+ Glass.SymbolKind_Object -> LSP.SymbolKind_Object+ Glass.SymbolKind_Key -> LSP.SymbolKind_Key+ Glass.SymbolKind_Null -> LSP.SymbolKind_Null+ Glass.SymbolKind_Enumerator -> LSP.SymbolKind_EnumMember+ Glass.SymbolKind_Struct -> LSP.SymbolKind_Struct+ Glass.SymbolKind_Event -> LSP.SymbolKind_Event+ Glass.SymbolKind_Operator -> LSP.SymbolKind_Operator+ Glass.SymbolKind_TypeParameter -> LSP.SymbolKind_TypeParameter+ Glass.SymbolKind_Union -> LSP.SymbolKind_Class -- ?+ Glass.SymbolKind_Macro -> LSP.SymbolKind_Function -- ?+ Glass.SymbolKind_Trait -> LSP.SymbolKind_Interface -- ?+ Glass.SymbolKind_Fragment -> LSP.SymbolKind_Struct -- ?+ Glass.SymbolKind_Operation -> LSP.SymbolKind_Method -- ?+ Glass.SymbolKind_Directive -> LSP.SymbolKind_Null -- ?+ _ -> LSP.SymbolKind_Null++attrSymbolSignature :: Glass.Attributes -> Maybe Text+attrSymbolSignature attrs =+ case Map.lookup "symbolSignature" attrs.unAttributes of+ Just (Glass.Attribute_aString ty) -> Just ty+ _ -> Nothing++attrSymbolName :: Glass.Attributes -> Maybe Text+attrSymbolName attrs =+ case Map.lookup "symbolName" attrs.unAttributes of+ Just (Glass.Attribute_aString nm) -> Just nm+ _ -> Nothing++attrSymbolKind :: Glass.Attributes -> Maybe LSP.SymbolKind+attrSymbolKind attrs =+ case Map.lookup "symbolKind" attrs.unAttributes of+ Just (Glass.Attribute_aInteger kind) ->+ Just (toLspSymbolKind (toThriftEnum (fromIntegral kind)))+ _ -> Nothing++-- -----------------------------------------------------------------------------+-- Utils++uriToAbsPath :: (MonadThrow m) => LSP.Uri -> m AbsPath+uriToAbsPath uri =+ case LSP.uriToFilePath uri of+ Nothing -> throwM $ GleanLspException $ "URI is not a file: " <> LSP.getUri uri+ Just path -> Path.filePathToAbsThrow path++absPathToUri :: AbsPath -> LSP.Uri+absPathToUri = LSP.filePathToUri . Path.toFilePath++locationToLocationLink :: LSP.Location -> LSP.LocationLink+locationToLocationLink LSP.Location {_uri, _range} =+ LSP.LocationLink+ { _originSelectionRange = Nothing+ , _targetUri = _uri+ , _targetRange = _range+ , _targetSelectionRange = _range+ }++logTimed :: MonadIO m => Text -> m a -> m a+logTimed msg io = do+ (t, b, a) <- timeIt io+ liftIO $ logInfo $ msg <> ": " <>+ Text.pack (showTime t) <> ", " <> Text.pack (showAllocs b)+ return a+
+ LICENSE view
@@ -0,0 +1,70 @@+BSD License++For Glean software++Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.++Redistribution and use in source and binary forms, with or without modification,+are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++ * Neither the name Facebook nor the names of its contributors may be used to+ endorse or promote products derived from this software without specific+ prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++Portions of this software were taken from+https://github.com/josephsumabat/static-ls, which is provided with the+following license:++Copyright (c) 2023 Joseph Sumabat++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE++The following license is also included for code taken from ghcide:++Copyright 2019 Digital Asset (Switzerland) GmbH and/or its affiliates++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.OFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,179 @@+# Generic Glean LSP Server++`glean-lsp` is a generic LSP server that provides a minimal set of LSP+features using Glean data. A Glean-based LSP is particularly suited+for navigating very large codebases, where it wouldn't be practical to+use a traditional language service that parses all the source files on+the fly.++The Glean LSP server works for any language supported by Glean and+Glass, and currently provides the following LSP features:++* Go to Definition+* Go to References+* Type on Hover+* Outline+* Symbol Search++More features may be added in the future.++The LSP server is purely static, in that it will not re-index the+source files if they are edited. To update the Glean data, currently+you have to manually re-run the indexer yourself. This can be done+without restarting VS Code or the LSP server, though.++## Building & installing++`cabal install glean-lsp`++## Using it with VS Code++First index your source code into a Glean DB. This will be something like++```+cd SRC+glean index LANGUAGE --db-root GLEANDB --db NAME/HASH .+```++where++* `SRC` is the directory containing your source code+* `LANGUAGE` is the language you want to index, see the [Glean manual](https://glean.software/docs/indexer/intro/) for available languages+* `GLEANDB` is where you want to store your DBs+* `NAME' is the name for the DB, and `HASH` can just be 0 or you could+ use the git hash of the repository if you want to index multiple+ versions of the code.++See the [Glean manual](https://glean.software/docs/indexer/intro/) for any language-specific options that you might need.++To use this LSP server with VS Code you need a generic LSP client such+as [Generic LSP Client (v2)](https://github.com/zsol/vscode-glspc). Using that client,+add the following to `.vscode/settings.json`+++```+[+ "glean-lsp": {+ "repo": "NAME"+ },+ "glspc.server.command": "GLEAN-LSP"+ "glspc.server.commandArguments": ["--db-root", "GLEANDB"],+ "glspc.server.languageId": [+ "LANGUAGE-ID"+ ]+}+```++where++* `GLEAN-LSP` is the path to the `glean-lsp` binary, which might be somethihng like `$HOME/.cabal/bin/glean-lsp` if you installed it with `cabal install glean-lsp`.+* `GLEANDB` is the path to your Glean DBs (same as when running the indexer above).+* `NAME` is the name of the Glean DB you want to use. Don't add+ `HASH` here, `glean-lsp` will always use the most recent version of+ the DB.+* `LANGUAGE-ID` is the language you want to enable this extension+ for. Make sure you also disable any other extensions you have for+ the same language, you can do that locally for this workspace in VS Code.++Now if you open this workspace in VS Code and open one of the source+files that was indexed, you should have go-to-definition and the other+features available.++## Example: browse the React codebase with Glean++Clone a copy of React:++```+$ git clone --depth 1 https://github.com/facebook/react.git+$ cd react+```++Start a Glean server:++```+$ glean-server --db-root .glean --port 1234 &+```++If you don't have `Flow` installed:++```+$ sudo npm install -g flow-bin+```++Index the code into a DB called `react/0`:++```+$ grep -v '^%' scripts/flow/config/flowconfig >.flowconfig+$ glean index flow . --write-root "" --service localhost:1234 --db react/0+```++Put the following into `/tmp/react/.vscode/settings`:++```+{+ "glean-lsp": {+ "repo": "react"+ },+ "glspc.server.command": "glean-lsp",+ "glspc.server.commandArguments": ["--service", "localhost:1234"],+ "glspc.server.languageId": [+ "typescript", "javascript"+ ]+}+```++Start VS Code, and open the folder `react` that you cloned. You+probably want to disable the built-in TypeScript extensions to avoid+conflicts: `Ctrl-Shift-X` and filter on built-in extensions (Show+Built-in Extensions in the `...` dropdown), then disable the+extensions for TypeScript for this workspace only.++Now open a `.js` file, and you should have navigation features+provided by `glean-lsp`: try right-click and "Go to Definition" or "Go+to References". Check that the Outline view is populated. Try symbol+search with `Ctrl-T`.++## Troubleshooting++In the "Output" pane, select the output for the LSP client in the+dropdown on the right to see the debugging output.++A common problem is the file paths not matching up, which will result+in the LSP server starting up and appearing to be working, but+go-to-definition just doesn't work. Make sure that in the Glean DB the+file paths are all relative to the root of the workspace. To check+this you can open the DB in the shell and query for `src.File _`. For+example when I look at my DB `stackage/1` which contains all the+Haskell packages in Stackage:++```+$ glean shell --db-root ~/gleandb --db stackage/1+Glean Shell, built on 2025-07-14 13:39:35.711312749 UTC, from rev <unknown>+Using local DBs from rocksdb:/home/simon/gleandb+type :help for help.+stackage> :limit 5+stackage> src.File _+{ "id": 66477, "key": "AC-Angle-1.0/Data/Angle.hs" }+{ "id": 19929124, "key": "ANum-0.2.0.2/src/Data/ANum.hs" }+{ "id": 7772678, "key": "Agda-2.6.4/src/full/Agda/Auto/Auto.hs" }+{ "id": 15530734, "key": "Agda-2.6.4/src/full/Agda/Auto/CaseSplit.hs" }+{ "id": 11442673, "key": "Agda-2.6.4/src/full/Agda/Auto/Convert.hs" }++5 results, 5 facts, 151.82ms, 208003368 bytes, 584 compiled bytes+results truncated (current limit 5, use :limit <n> to change it)+Use :more to see more results+stackage> +```++Check that the paths look reasonable. If not, you may need to use+language-specific options to get the indexer to generate the right+paths.++If the paths look OK, things should work to the extent that support+for the language is implemented in Glean's `codemarkup` layer and the+Glass server. Some languages might be missing certain features, but+the basics should work for most languages. If you're still having+trouble try raising an issue on the [Glean issue+tracker](https://github.com/facebookincubator/Glean/issues) or asking+on Discord, details of which are in Glean's+[README](https://github.com/facebookincubator/Glean/blob/main/README.md).
+ glean-lsp.cabal view
@@ -0,0 +1,93 @@+cabal-version: 3.6+name: glean-lsp+version: 0.1.0.0+synopsis: Generic Glean-based LSP Server+homepage: https://github.com/facebookincubator/Glean+bug-reports: https://github.com/facebookincubator/Glean/issues+license: BSD-3-Clause+license-file: LICENSE+author: Facebook, Inc.+maintainer: Glean-team@fb.com+copyright: (c) Facebook, All Rights Reserved+build-type: Simple+extra-doc-files: CHANGELOG.md, README.md++common fb-haskell+ default-language: Haskell2010+ default-extensions:+ BangPatterns+ BinaryLiterals+ DataKinds+ DeriveDataTypeable+ DeriveGeneric+ EmptyCase+ ExistentialQuantification+ FlexibleContexts+ FlexibleInstances+ GADTs+ GeneralizedNewtypeDeriving+ LambdaCase+ MultiParamTypeClasses+ MultiWayIf+ NamedFieldPuns+ NoMonomorphismRestriction+ OverloadedStrings+ PatternSynonyms+ RankNTypes+ ScopedTypeVariables+ StandaloneDeriving+ TupleSections+ TypeFamilies+ TypeSynonymInstances+ NondecreasingIndentation+ TypeOperators+ -- these are new relative to regular fb-haskell:+ DisambiguateRecordFields+ OverloadedRecordDot+ NoFieldSelectors+ DuplicateRecordFields+ ImportQualifiedPost++ ghc-options:+ -Wall+ -Wno-orphans+ -Wno-name-shadowing+ -Wno-unticked-promoted-constructors+ if flag(opt)+ ghc-options: -O2++flag opt+ default: False++common exe+ ghc-options: -threaded -rtsopts++executable glean-lsp+ import: fb-haskell, exe+ main-is: Glean/LSP.hs+ hs-source-dirs: .+ ghc-options: -main-is Glean.LSP+ other-modules:+ Data.Path+ Data.ConcurrentCache+ build-depends:+ aeson >= 2.0.3 && < 2.3,+ base >=4.11.1 && <4.19,+ containers >= 0.6 && < 0.7,+ data-default >= 0.7.1 && < 0.9,+ directory ^>=1.3.1.5,+ exceptions ^>=0.10.0,+ filepath >= 1.4.2 && < 1.5,+ hashable >=1.2.7.0 && <1.5,+ lsp >=2.7.0.0 && <2.8.0.0,+ text >=1.2.3.0 && < 2.2,+ transformers >= 0.5.6 && < 0.7,+ unliftio ^>=0.2,+ unliftio-core ^>=0.2,+ unordered-containers >= 0.2.9.0 && < 0.3,+ optparse-applicative >= 0.17 && < 0.19,+ glean:glass-lib >= 0.2 && < 0.3,+ glean:if-glass-hs,+ glean:util,+ fb-util,+ thrift-lib