packages feed

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 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