From 6e5933f02fc22ea8a759ba22a960bc6225839813 Mon Sep 17 00:00:00 2001 From: Joseph Sumabat Date: Sun, 13 Apr 2025 04:34:28 -0400 Subject: [PATCH 01/21] Compatibility patch with haskell-arborist --- cabal.project | 15 +- expect_tests/HirTest.hs | 3 +- expect_tests/Main.hs | 2 +- nix/overlays/default.nix | 15 +- package.yaml | 1 + src/Data/Path.hs | 104 ---- src/StaticLS/Hir.hs | 7 - src/StaticLS/Hir/Make.hs | 30 -- src/StaticLS/Hir/Name.hs | 1 - src/StaticLS/Hir/Parse.hs | 505 ------------------ src/StaticLS/Hir/Print.hs | 37 -- src/StaticLS/Hir/Types.hs | 222 -------- src/StaticLS/IDE/CodeActions/AddTypeSig.hs | 12 +- src/StaticLS/IDE/CodeActions/AutoExport.hs | 20 +- src/StaticLS/IDE/CodeActions/AutoImport.hs | 7 +- .../IDE/CodeActions/RemoveRedundantImports.hs | 8 +- src/StaticLS/IDE/Completion.hs | 10 +- src/StaticLS/IDE/Definition.hs | 3 +- src/StaticLS/IDE/DocumentSymbols.hs | 34 +- src/StaticLS/IDE/Hover.hs | 5 +- .../IDE/InlayHints/TypeAnnotations.hs | 20 +- src/StaticLS/IDE/InlayHints/Wildcard.hs | 10 +- src/StaticLS/IDE/Monad.hs | 5 +- src/StaticLS/IDE/References.hs | 3 +- src/StaticLS/IDE/Rename.hs | 15 +- src/StaticLS/IDE/Utils.hs | 3 +- src/StaticLS/Semantic.hs | 5 +- src/StaticLS/Tree.hs | 12 +- static-ls.cabal | 12 +- test/Semantic/HirSpec.hs | 2 +- test/StaticLS/HirSpec.hs | 2 +- test/StaticLS/IDE/RenameSpec.hs | 2 +- 32 files changed, 118 insertions(+), 1014 deletions(-) delete mode 100644 src/Data/Path.hs delete mode 100644 src/StaticLS/Hir.hs delete mode 100644 src/StaticLS/Hir/Make.hs delete mode 100644 src/StaticLS/Hir/Name.hs delete mode 100644 src/StaticLS/Hir/Parse.hs delete mode 100644 src/StaticLS/Hir/Print.hs delete mode 100644 src/StaticLS/Hir/Types.hs diff --git a/cabal.project b/cabal.project index 9cf26d20..73879fb2 100644 --- a/cabal.project +++ b/cabal.project @@ -8,33 +8,38 @@ packages: source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 64f8a19b7e65a4a572770a92085f872caf212833 + tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e subdir: tree-sitter-simple source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 64f8a19b7e65a4a572770a92085f872caf212833 + tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e subdir: tree-sitter-haskell source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 64f8a19b7e65a4a572770a92085f872caf212833 + tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e subdir: tree-sitter-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 64f8a19b7e65a4a572770a92085f872caf212833 + tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e subdir: haskell-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 64f8a19b7e65a4a572770a92085f872caf212833 + tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e subdir: text-range +source-repository-package + type: git + location: https://github.com/josephsumabat/haskell-arborist + tag: d69de64fcf2cd729816b18f1a0a410c198de62af + source-repository-package type: git location: https://github.com/oberblastmeister/tasty-expect.git diff --git a/expect_tests/HirTest.hs b/expect_tests/HirTest.hs index 2139c791..070004f9 100644 --- a/expect_tests/HirTest.hs +++ b/expect_tests/HirTest.hs @@ -6,7 +6,8 @@ import AST.Haskell qualified as H import Data.Text qualified as T import Data.Text.Lazy qualified as TL import NeatInterpolation -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir +import Hir.Parse qualified as Hir import Test.Tasty import Test.Tasty.Expect import Text.Pretty.Simple qualified as Pretty diff --git a/expect_tests/Main.hs b/expect_tests/Main.hs index e44168a8..aac48bc8 100644 --- a/expect_tests/Main.hs +++ b/expect_tests/Main.hs @@ -6,7 +6,7 @@ import Test.Tasty.Expect main :: IO () main = do - defaultMainWithIngredients (expectIngredient : defaultIngredients) tests + --defaultMainWithIngredients (expectIngredient : defaultIngredients) tests pure () tests :: TestTree diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index b4d51212..3a1ffc1a 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -2,8 +2,15 @@ let tree-sitter-simple-repo = { url = "https://github.com/josephsumabat/tree-sitter-simple"; - sha256 = "sha256-Taje8q2fYZzA68sSt8f9/oCDdYjTWegfoYusQtmrz8A="; - rev = "64f8a19b7e65a4a572770a92085f872caf212833"; + sha256 = "sha256-X4SM6v9CMs3DeX7thhV+8bEmVxDNdMGaqHr9t0dDdYw="; + rev = "03085cb9e6f1c7850e0c3bcd227ba0c730c7013e"; + fetchSubmodules = true; + }; + + haskell-arborist-repo = { + url = "https://github.com/josephsumabat/haskell-arborist"; + sha256 = "sha256-7KhlcDsrdxHwEl3FCa6WHneP7En6Luqfd9mUQw+BrP4="; + rev = "d69de64fcf2cd729816b18f1a0a410c198de62af"; fetchSubmodules = true; }; @@ -20,6 +27,10 @@ in haskellPackages = super.haskell.packages.${self.ghcVersion}.override { overrides = haskellSelf: haskellSuper: { + haskell-arborist = + haskellSuper.callCabal2nix + "haskell-arborist" "${(super.fetchgit haskell-arborist-repo)}" {}; + tree-sitter-haskell = haskellSuper.callCabal2nix "tree-sitter-haskell" "${(super.fetchgit tree-sitter-simple-repo)}/tree-sitter-haskell" {}; diff --git a/package.yaml b/package.yaml index 56f486e3..9c22fd3a 100644 --- a/package.yaml +++ b/package.yaml @@ -68,6 +68,7 @@ dependencies: - row-types - regex-tdfa # why is the api for this library so bad - stm + - haskell-arborist language: GHC2021 default-extensions: - LambdaCase diff --git a/src/Data/Path.hs b/src/Data/Path.hs deleted file mode 100644 index f92352c3..00000000 --- a/src/Data/Path.hs +++ /dev/null @@ -1,104 +0,0 @@ -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 = 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) diff --git a/src/StaticLS/Hir.hs b/src/StaticLS/Hir.hs deleted file mode 100644 index b2e69c93..00000000 --- a/src/StaticLS/Hir.hs +++ /dev/null @@ -1,7 +0,0 @@ -{-# LANGUAGE QuasiQuotes #-} - -module StaticLS.Hir (module X) where - -import StaticLS.Hir.Parse as X -import StaticLS.Hir.Print as X -import StaticLS.Hir.Types as X diff --git a/src/StaticLS/Hir/Make.hs b/src/StaticLS/Hir/Make.hs deleted file mode 100644 index 8fcd09ca..00000000 --- a/src/StaticLS/Hir/Make.hs +++ /dev/null @@ -1,30 +0,0 @@ -module StaticLS.Hir.Make where - -import AST qualified -import Data.List.NonEmpty (NonEmpty) -import Data.List.NonEmpty qualified as NE -import Data.Text (Text) -import Data.Text qualified as T -import StaticLS.Hir.Types - -mkName :: Text -> Bool -> Bool -> Name -mkName name isOperator isConstructor = - Name - { node = AST.defaultNode {AST.nodeText = name} - , isOperator - , isConstructor - } - -mkModuleText :: NonEmpty Text -> ModuleText -mkModuleText parts = - ModuleText - { parts - , text = T.intercalate "." (NE.toList parts) - } - --- mkModuleName :: NonEmpty Text -> ModuleName --- mkModuleName parts = --- ModuleName --- { mod = mkModuleText parts --- , node = AST.defaultNode --- } diff --git a/src/StaticLS/Hir/Name.hs b/src/StaticLS/Hir/Name.hs deleted file mode 100644 index 4d0c46d4..00000000 --- a/src/StaticLS/Hir/Name.hs +++ /dev/null @@ -1 +0,0 @@ -module StaticLS.Hir.Name where diff --git a/src/StaticLS/Hir/Parse.hs b/src/StaticLS/Hir/Parse.hs deleted file mode 100644 index 5cc61d9c..00000000 --- a/src/StaticLS/Hir/Parse.hs +++ /dev/null @@ -1,505 +0,0 @@ -{-# LANGUAGE QuasiQuotes #-} - -module StaticLS.Hir.Parse where - -import AST (DynNode) -import AST qualified -import AST.Haskell qualified as H -import AST.Sum ((:+)) -import Control.Applicative (asum, (<|>)) -import Control.Error (hush) -import Control.Error qualified as Error -import Control.Error.Util (note) -import Control.Monad (guard) -import Data.Either qualified as Either -import Data.List.NonEmpty qualified as NE -import Data.Maybe qualified as Maybe -import Data.Range (Range) -import Data.Text (Text) -import Data.Text qualified as T -import StaticLS.Hir.Types - -parseName :: ParseNameTypes -> Name -parseName ast = case ast of - AST.Inj @H.Name _ -> - Name - { node - , isOperator = False - , isConstructor = False - } - AST.Inj @H.Constructor _ -> - Name - { node - , isOperator = False - , isConstructor = True - } - AST.Inj @H.Variable _ -> - Name - { node - , isOperator = False - , isConstructor = False - } - AST.Inj @H.Operator _ -> - Name - { node - , isOperator = True - , isConstructor = False - } - AST.Inj @H.FieldName _ -> - Name - { node - , isOperator = False - , isConstructor = False - } - AST.Inj @H.ConstructorOperator _ -> - Name - { node - , isOperator = True - , isConstructor = True - } - _ -> error "could not parse name" - where - node = AST.getDynNode ast - -parseNamePrefix :: (H.PrefixId :+ H.PrefixList :+ H.Unit :+ ParseNameTypes) -> AST.Err Name -parseNamePrefix node = - case node of - [AST.x1|prefixId|] -> do - prefixId <- AST.unwrap prefixId - parseName <$> removeQualified prefixId.children - [AST.x2|prefixList|] -> do - pure - Name - { node = prefixList.dynNode - , isOperator = True - , isConstructor = True - } - [AST.x3|unit|] -> do - pure - Name - { node = unit.dynNode - , isOperator = False - , isConstructor = True - } - [AST.rest3|name|] -> pure $ parseName name - -parseModuleTextFromText :: Text -> ModuleText -parseModuleTextFromText text = - ModuleText - { parts = NE.fromList (T.splitOn "." text) - , text - } - -importQualifier :: Import -> ModuleText -importQualifier i = - -- even if something is not imported qualified, - -- it still produced a namespace that can be used as a qualifier - -- for example - -- `import Data.Text` - -- allows you to use `Data.Text.Text` with the qualifier - -- or just `FilePath` without the qualifier - Maybe.fromMaybe i.mod i.alias - -findNode :: (AST.DynNode -> Maybe b) -> AST.DynNode -> Maybe b -findNode f n = go n - where - go n = f n <|> asum (go <$> (AST.nodeChildren n)) - -parseImportName :: H.ImportName -> AST.Err ImportName -parseImportName name = do - let text = AST.nodeToText name - pure $ ImportName {name = text} - -parseNameSpace :: H.Namespace -> AST.Err NameSpace -parseNameSpace n = case n.dynNode.nodeText of - "data" -> pure NameSpaceValue - "type" -> pure NameSpaceType - "pattern" -> pure NameSpacePattern - _ -> Left $ "could not parse namespace: " <> T.pack (show n) - -parseImportOperator :: H.PrefixId -> AST.Err Name -parseImportOperator operator = do - operator <- operator.children - parseName <$> removeQualified operator - -parseExportOperator :: H.PrefixId -> AST.Err Qualified -parseExportOperator operator = do - operator <- operator.children - parseQualified $ AST.subset operator - -removeQualified :: forall n n'. (AST.Subset n (H.Qualified :+ n')) => n -> AST.Err n' -removeQualified n = case AST.subset @_ @(H.Qualified :+ n') n of - AST.X qualified -> Left $ "qualified name in import: " <> qualified.dynNode.nodeText - AST.Rest name -> pure name - -type ParseImportChildren = H.Qualified :+ H.AllNames :+ H.AssociatedType :+ H.PrefixId :+ ParseNameTypes - -parseImportChild :: ParseImportChildren -> AST.Err ImportChildren -parseImportChild child = case child of - [AST.x1|qualified|] -> Left $ "qualified name in import children: " <> qualified.dynNode.nodeText - [AST.x2|_allNames|] -> pure ImportAllChildren - [AST.x3|assocType|] -> do - type' <- assocType.type' - name <- removeQualified type' - pure $ ImportChild NameSpaceType (parseName name) - [AST.x4|prefixId|] -> do - operator <- prefixId.children - name <- removeQualified operator - pure $ ImportChild NameSpaceValue (parseName name) - [AST.rest4|rest|] -> pure $ ImportChild NameSpaceValue (parseName rest) - -parseExportChild :: ParseImportChildren -> AST.Err ExportChildren -parseExportChild child = case child of - [AST.x1|qualified|] -> do - name <- parseQualified (AST.Inj qualified) - pure $ ExportChild NameSpaceValue name - [AST.x2|_allNames|] -> pure ExportAllChildren - [AST.x3|assocType|] -> do - type' <- assocType.type' - name <- parseQualified $ AST.subset type' - pure $ ExportChild NameSpaceType name - [AST.x4|prefixId|] -> do - operator <- prefixId.children - name <- parseQualified $ AST.subset operator - pure $ ExportChild NameSpaceValue name - [AST.rest4|rest|] -> do - name <- parseQualified (AST.subset rest) - pure $ ExportChild NameSpaceValue name - -parseImportChildren :: H.Children -> AST.Err [ImportChildren] -parseImportChildren children = do - element <- AST.collapseErr children.element - let children = AST.subset @_ @ParseImportChildren <$> element - children <- traverse parseImportChild children - pure children - -parseImportItem :: H.ImportName -> AST.Err ImportItem -parseImportItem i = do - namespace <- traverse parseNameSpace =<< AST.collapseErr i.namespace - namespace <- pure $ Maybe.fromMaybe NameSpaceValue namespace - name <- do - operator <- traverse parseImportOperator =<< AST.collapseErr i.operator - type' <- traverse (fmap parseName . removeQualified) =<< AST.collapseErr i.type' - variable <- traverse (fmap parseName . removeQualified) =<< AST.collapseErr i.variable - case operator <|> type' <|> variable of - Just n -> pure n - Nothing -> Left "could not parse import name" - children <- traverse parseImportChildren =<< AST.collapseErr i.children' - children <- pure $ Maybe.fromMaybe [] children - pure - ImportItem - { namespace - , name - , children - } - -parseExportChildren :: H.Children -> AST.Err [ExportChildren] -parseExportChildren children = do - element <- AST.collapseErr children.element - let children = AST.subset @_ @ParseImportChildren <$> element - children <- traverse parseExportChild children - pure children - -parseExportItem :: H.Export -> AST.Err ExportItem -parseExportItem e = do - e <- AST.unwrap e - namespace <- traverse parseNameSpace e.namespace - namespace <- pure $ Maybe.fromMaybe NameSpaceValue namespace - name <- do - operator <- traverse parseExportOperator e.operator - type' <- traverse (parseQualified . AST.subset) e.type' - variable <- traverse (parseQualified . AST.subset) e.variable - case operator <|> type' <|> variable of - Just n -> pure n - Nothing -> Left "could not parse import name" - children <- traverse parseExportChildren e.children' - children <- pure $ Maybe.fromMaybe [] children - pure - ExportItem - { namespace - , name - , children - } -parseModuleExportItem :: H.ModuleExport -> AST.Err ExportItem -parseModuleExportItem e = do - module' <- parseModuleName =<< e.module' - pure $ ExportModuleItem module' - -parseExportList :: H.Exports -> AST.Err [ExportItem] -parseExportList exports = do - export <- AST.collapseErr exports.export - moduleExports <- AST.collapseErr exports.children - normalExports <- traverse parseExportItem export - moduleExports <- traverse parseModuleExportItem moduleExports - pure $ normalExports ++ moduleExports - -parseImportList :: H.ImportList -> AST.Err [ImportItem] -parseImportList i = do - name <- AST.collapseErr i.name - items <- traverse parseImportItem name - pure items - -parseModuleText :: H.Module -> AST.Err ModuleText -parseModuleText m = do - ids <- AST.collapseErr m.children - pure $ - ModuleText - { text = - -- the text sometimes includes trailing dots - T.dropWhileEnd (== '.') (AST.nodeToText m) - , parts = fmap AST.nodeToText ids - } - -parseModuleName :: H.Module -> AST.Err ModuleName -parseModuleName m = do - mod <- parseModuleText m - pure $ ModuleName {mod, node = m} - -parseImport :: H.Import -> AST.Err Import -parseImport i = do - mod <- i.module' - mod <- parseModuleText mod - alias <- AST.collapseErr i.alias - alias <- traverse parseModuleText alias - importList <- AST.collapseErr i.names - importList <- traverse parseImportList importList - importList <- pure $ Maybe.fromMaybe [] importList - let qualified = Maybe.isJust $ findNode (AST.cast @(AST.Token "qualified")) (AST.getDynNode i) - let hiding = Maybe.isJust $ findNode (AST.cast @(AST.Token "hiding")) (AST.getDynNode i) - pure - Import - { mod - , alias - , qualified - , hiding - , importList - } - -parseQualified :: ParseQualifiedTypes -> AST.Err Qualified -parseQualified q = do - case q of - AST.X q -> do - mod <- q.module' - mod <- parseModuleName mod - name <- q.id - let name' = AST.subset @_ @ParseNameTypes name - name <- pure $ parseName name' - pure $ Qualified {mod = Just mod, name} - AST.Rest q -> do - let name = parseName q - pure $ Qualified {mod = Nothing, name} - -getQualifiedAtPoint :: Range -> H.Haskell -> AST.Err (Maybe Qualified) -getQualifiedAtPoint range h = do - let node = AST.getDeepestContaining @H.Qualified range (AST.getDynNode h) - case node of - Nothing -> - traverse - parseQualified - (AST.getDeepestContaining @ParseQualifiedTypes range h.dynNode) - Just node -> Just <$> parseQualified (AST.Inj node) - -parseImports :: H.Imports -> AST.Err ([Text], [Import]) -parseImports i = do - import' <- i.import' - let (es, imports) = Either.partitionEithers (NE.toList import') - imports <- pure $ parseImport <$> imports - let (es', imports') = Either.partitionEithers imports - pure (es ++ es', imports') - -parseDataType :: H.DataType -> AST.Err DataDecl -parseDataType node = do - dt <- AST.unwrap node - name <- Error.note "no name for data type" dt.name - name <- parseNamePrefix =<< removeQualified name - pure DataDecl {name, node} - -parseBind :: H.Decl -> AST.Err (Decl) -parseBind decl = do - case decl.getDecl of - [AST.x1|bindNode|] -> do - bind <- AST.unwrap bindNode - name <- Error.note "no bind name" bind.name - name <- parseNamePrefix $ AST.subset name - pure $ DeclBind BindDecl {name, node = AST.Inj bindNode} - [AST.x2|fnNode|] -> do - fn <- AST.unwrap fnNode - name <- Error.note "no function name" fn.name - name <- parseNamePrefix $ AST.subset name - pure $ DeclBind BindDecl {name, node = AST.Inj fnNode} - [AST.x3|sigNode|] -> do - sig <- AST.unwrap sigNode - name <- Error.note "no signature name" sig.name - name <- parseNamePrefix $ AST.subset name - pure $ DeclSig SigDecl {name, node = sigNode} - [AST.rest3|nil|] -> case nil of {} - -parseClass :: H.Class -> AST.Err (Decl) -parseClass c = do - cu <- AST.unwrap c - name <- note "no name for class" cu.name - name <- parseNamePrefix $ AST.subset name - pure $ DeclClass ClassDecl {name, node = c} - -parseDataFamily :: H.DataFamily -> AST.Err (Decl) -parseDataFamily d = do - du <- AST.unwrap d - name <- note "no name for data family" du.name - name <- parseNamePrefix $ AST.subset name - pure $ DeclDataFamily DataFamilyDecl {name, node = d} - -parseNewtype :: H.Newtype -> AST.Err (Decl) -parseNewtype n = do - nu <- AST.unwrap n - name <- note "no name for newtype" nu.name - name <- parseNamePrefix =<< removeQualified name - pure $ DeclNewtype NewtypeDecl {name, node = n} - -parseBindingList :: H.BindingList -> AST.Err [Name] -parseBindingList bs = do - bsu <- AST.unwrap bs - let names = NE.toList bsu.name - traverse (parseNamePrefix . AST.subset) names - -parsePattern :: H.PatternSynonym -> AST.Err ([Decl]) -parsePattern p = do - p <- p.children - case p of - AST.Inj @H.Equation e -> do - eu <- AST.unwrap e - synonym <- note "no synonym" eu.synonym - res <- note "no name found" $ AST.getDeepestSatisfying (AST.cast @ParseNameTypes) (AST.getDynNode synonym) - let name = parseName res - pure [DeclPattern PatternDecl {name, node = e}] - AST.Inj @H.Signature s -> do - su <- AST.unwrap s - case su.names of - Just bindingList -> do - names <- parseBindingList bindingList - pure $ fmap (\name -> DeclPatternSig PatternSigDecl {name, node = s}) names - Nothing -> do - synonym <- note "no synonym" su.synonym - case synonym of - AST.X bindingList -> do - names <- parseBindingList bindingList - pure $ fmap (\name -> DeclPatternSig PatternSigDecl {name, node = s}) names - AST.Rest name -> do - name <- parseNamePrefix =<< removeQualified name - pure [DeclPatternSig PatternSigDecl {name, node = s}] - _ -> pure [] - -parseTypeFamily :: H.TypeFamily -> AST.Err (Decl) -parseTypeFamily t = do - tu <- AST.unwrap t - name <- note "no name for type family" tu.name - name <- parseNamePrefix =<< removeQualified name - pure $ DeclTypeFamily TypeFamilyDecl {name, node = t} - -parseTypeSynonym :: H.TypeSynomym -> AST.Err (Decl) -parseTypeSynonym t = do - tu <- AST.unwrap t - name <- note "no name for type synonym" tu.name - name <- parseNamePrefix =<< removeQualified name - pure $ DeclTypeSynonym TypeSynonymDecl {name, node = t} - -parseDeclaration :: H.Declaration -> AST.Err ([Decl]) -parseDeclaration decl = case decl.getDeclaration of - AST.Inj @H.DataType d -> do - (pure @[] . DeclData) <$> parseDataType d - AST.Inj @H.Decl b -> pure @[] <$> parseBind b - AST.Inj @H.Class c -> pure @[] <$> parseClass c - AST.Inj @H.DataFamily d -> pure @[] <$> parseDataFamily d - AST.Inj @H.Newtype n -> pure @[] <$> parseNewtype n - AST.Inj @H.PatternSynonym p -> parsePattern p - AST.Inj @H.TypeFamily t -> pure @[] <$> parseTypeFamily t - AST.Inj @H.TypeSynomym t -> pure @[] <$> parseTypeSynonym t - _ -> pure [] - -emptyProgram :: Program -emptyProgram = - Program - { imports = [] - , exports = [] - , decls = [] - } - -parseHaskell :: H.Haskell -> ([Text], Program) -parseHaskell h = do - let res = do - let imports = Maybe.fromMaybe Nothing $ Error.hush $ AST.collapseErr h.imports - (es, imports) <- case imports of - Nothing -> pure ([], []) - Just imports -> parseImports imports - header <- AST.collapseErr h.children - (es', exports) <- case header of - Nothing -> pure (es, []) - Just header -> do - let exports = Maybe.fromMaybe Nothing $ Error.hush $ AST.collapseErr header.exports - let exports' = Maybe.fromMaybe [] $ Maybe.fromMaybe Nothing $ Error.hush $ traverse parseExportList exports - pure (es, exports') - (es'', decls) <- do - let decls = Maybe.fromMaybe Nothing $ Error.hush $ AST.collapseErr h.declarations - case decls of - Nothing -> pure ([], []) - Just decls -> do - -- let children = - let children = Maybe.fromMaybe [] $ fmap NE.toList $ Error.hush $ AST.collapseErr decls.children - let parseChild (child :: H.Declaration :+ H.Import :+ AST.Nil) = do - case child of - AST.X decl -> do - decl <- parseDeclaration decl - pure decl - AST.Rest (AST.X _imp) -> do - Left "cannot have import in declaration list" - AST.Rest (AST.Rest nil) -> case nil of {} - let decls = fmap parseChild children - let (es, decls') = Either.partitionEithers decls - let decls'' = concat decls' - pure (es, decls'') - pure (es ++ es' ++ es'', Program {imports, exports, decls}) - case res of - Right (es, program) -> (es, program) - Left e -> ([e], emptyProgram) - -getNameTypes :: Range -> H.Haskell -> Maybe GetNameTypes -getNameTypes range hs = AST.getDeepestContaining @GetNameTypes range hs.dynNode - -parseThQuotedName :: H.ThQuotedName -> AST.Err ThQuotedName -parseThQuotedName thQuotedName = do - name <- AST.collapseErr thQuotedName.name - type' <- AST.collapseErr thQuotedName.type' - case (ThQuotedName False . AST.getDynNode <$> name) - <|> (ThQuotedName True . AST.getDynNode <$> type') of - Just text -> pure text - Nothing -> Left "ThQuotedName must have either a name or a type" - -getPersistentModelAtPoint :: Range -> H.Haskell -> Maybe Text -getPersistentModelAtPoint range hs = do - splice <- AST.getDeepestContaining @H.TopSplice range hs.dynNode - _ <- AST.getDeepestSatisfying getMkModelApply splice.dynNode - modelFileArg <- AST.getDeepestSatisfying getModelFileApply splice.dynNode - (AST.Inj @H.Literal modelFileLit) <- pure modelFileArg.getExpression - modelFileLit <- hush $ AST.unwrap modelFileLit - (AST.Inj @H.String modelFileStr) <- pure modelFileLit.children - let persistentModelName = modelFileStr.dynNode.nodeText - persistentModelName <- T.stripPrefix "\"" persistentModelName - persistentModelName <- T.stripSuffix "\"" persistentModelName - pure persistentModelName - where - getMkModelApply :: DynNode -> Maybe H.Expression - getMkModelApply = getApplyVarWithName "mkModel" - - getModelFileApply :: DynNode -> Maybe H.Expression - getModelFileApply = getApplyVarWithName "modelFile" - - getApplyVarWithName :: Text -> DynNode -> Maybe H.Expression - getApplyVarWithName name node = do - apply <- AST.cast @H.Apply node - apply <- hush $ AST.unwrap apply - fun <- apply.function - (AST.Inj @H.Expression funExpr) <- pure fun - (AST.Inj @H.Variable funVar) <- pure funExpr.getExpression - let funText = funVar.dynNode.nodeText - guard $ funText == name - (AST.Inj @H.Expression argExpr) <- pure apply.argument - pure argExpr diff --git a/src/StaticLS/Hir/Print.hs b/src/StaticLS/Hir/Print.hs deleted file mode 100644 index 7a05d555..00000000 --- a/src/StaticLS/Hir/Print.hs +++ /dev/null @@ -1,37 +0,0 @@ -module StaticLS.Hir.Print ( - printExportItem, - printExportItems, -) -where - -import AST qualified -import Data.Text.Lazy qualified as TL -import StaticLS.Hir.Types - -printName :: Name -> TL.Text -printName name = TL.fromStrict name.node.nodeText - -printModuleName :: ModuleName -> TL.Text -printModuleName name = TL.fromStrict name.mod.text - -printQualified :: Qualified -> TL.Text -printQualified (Qualified {mod, name}) = - case mod of - Nothing -> printName name - Just modName -> TL.concat [printModuleName modName, ".", printName name] - -printExportChildren :: ExportChildren -> TL.Text -printExportChildren export = - case export of - ExportAllChildren -> ".." - ExportChild _namespace name -> printQualified name - -printExportItem :: ExportItem -> TL.Text -printExportItem export = - case export of - ExportItem {namespace = _namespace, name, children} -> - printQualified name <> TL.intercalate ", " (map printExportChildren children) - ExportModuleItem mod -> "module " <> printModuleName mod - -printExportItems :: [ExportItem] -> TL.Text -printExportItems exports = TL.intercalate ",\n" (map printExportItem exports) diff --git a/src/StaticLS/Hir/Types.hs b/src/StaticLS/Hir/Types.hs deleted file mode 100644 index 117ba72f..00000000 --- a/src/StaticLS/Hir/Types.hs +++ /dev/null @@ -1,222 +0,0 @@ -module StaticLS.Hir.Types where - -import AST (DynNode) -import AST qualified -import AST.Haskell qualified as H -import AST.Haskell qualified as Haskell -import AST.Sum (Nil, (:+)) -import Data.Function (on) -import Data.Hashable (Hashable (..)) -import Data.List.NonEmpty (NonEmpty) -import Data.Text (Text) -import GHC.Generics (Generic) - -data NameSpace - = NameSpaceValue - | NameSpaceType - | NameSpacePattern - deriving (Show, Eq, Generic) - -instance Hashable NameSpace - -type ThSplice = Haskell.TopSplice :+ Haskell.Splice - -data Name = Name - { node :: !DynNode - , isOperator :: !Bool - , isConstructor :: !Bool - } - -data NameShow = NameShow {name :: Text, node :: DynNode} - deriving (Show) - -instance Show Name where - show Name {node} = show NameShow {name = AST.nodeToText node, node} - -instance Eq Name where - (==) = (==) `on` (.node.nodeText) - -instance Hashable Name where - hashWithSalt salt name = hashWithSalt salt name.node.nodeText - -data Qualified = Qualified - { mod :: Maybe ModuleName - , name :: Name - } - deriving (Show, Eq, Generic) - -instance Hashable Qualified - -data ModuleText = ModuleText - { parts :: NonEmpty Text - , text :: Text - } - deriving (Show) - -instance Eq ModuleText where - (==) = (==) `on` (.text) - -instance Hashable ModuleText where - hashWithSalt salt ModuleText {text} = hashWithSalt salt text - -data ModuleName = ModuleName - { mod :: ModuleText - , node :: H.Module - } - deriving (Show) - -instance Eq ModuleName where - (==) = (==) `on` (.mod) - -instance Hashable ModuleName where - hashWithSalt salt ModuleName {mod} = hashWithSalt salt mod - -data ImportChildren - = ImportAllChildren - | ImportChild NameSpace Name - deriving (Show) - -data ImportItem = ImportItem - { namespace :: NameSpace - , name :: Name - , children :: [ImportChildren] - } - deriving (Show) - -data ExportChildren - = ExportAllChildren - | ExportChild NameSpace Qualified - deriving (Show, Eq, Generic) - -instance Hashable ExportChildren - -data ExportItem - = ExportItem - { namespace :: NameSpace - , name :: Qualified - , children :: [ExportChildren] - } - | ExportModuleItem ModuleName - deriving (Show, Eq, Generic) - -instance Hashable ExportItem - -data ImportName = ImportName - { name :: Text - } - deriving (Show, Eq) - -data Import = Import - { mod :: ModuleText - , alias :: Maybe ModuleText - , qualified :: !Bool - , hiding :: !Bool - , importList :: [ImportItem] - } - deriving (Show) - -pattern OpenImport :: ModuleText -> Import -pattern OpenImport mod = Import {mod, alias = Nothing, qualified = False, hiding = False, importList = []} - -type ParseNameTypes = - Haskell.Name - :+ Haskell.Constructor - :+ Haskell.Variable - :+ Haskell.Operator - :+ Haskell.FieldName - :+ Haskell.ConstructorOperator - :+ Nil - -type ParseQualifiedTypes = H.Qualified :+ ParseNameTypes - -data DataDecl = DataDecl - { name :: Name - , node :: H.DataType - } - deriving (Show) - -data ClassDecl = ClassDecl - { name :: Name - , node :: H.Class - } - deriving (Show) - -data BindDecl = BindDecl - { name :: Name - , node :: H.Bind :+ H.Function :+ AST.Nil - } - deriving (Show) - -data SigDecl = SigDecl - { name :: Name - , node :: H.Signature - } - deriving (Show) - -data DataFamilyDecl = DataFamilyDecl - {name :: Name, node :: H.DataFamily} - deriving (Show) - -data NewtypeDecl = NewtypeDecl - { name :: Name - , node :: H.Newtype - } - deriving (Show) - -data PatternSigDecl = PatternSigDecl - { name :: Name - , node :: H.Signature - } - deriving (Show) - -data PatternDecl = PatternDecl - { name :: Name - , node :: H.Equation - } - deriving (Show) - -data TypeFamilyDecl = TypeFamilyDecl - { name :: Name - , node :: H.TypeFamily - } - deriving (Show) - -data TypeSynonymDecl = TypeSynonymDecl - { name :: Name - , node :: H.TypeSynomym - } - deriving (Show) - -data Decl - = DeclData DataDecl - | DeclNewtype NewtypeDecl - | DeclClass ClassDecl - | DeclSig SigDecl - | DeclBind BindDecl - | DeclDataFamily DataFamilyDecl - | DeclPatternSig PatternSigDecl - | DeclPattern PatternDecl - | DeclTypeFamily TypeFamilyDecl - | DeclTypeSynonym TypeSynonymDecl - deriving (Show) - -data Program = Program - { imports :: [Import] - , exports :: [ExportItem] - , decls :: [Decl] - } - deriving (Show) - -type GetNameTypes = - Haskell.Name - :+ Haskell.Constructor - :+ Haskell.Variable - :+ Haskell.Operator - :+ Haskell.FieldName - :+ Haskell.ConstructorOperator - :+ Nil - -data ThQuotedName = ThQuotedName - { isTy :: Bool - , node :: AST.DynNode - } diff --git a/src/StaticLS/IDE/CodeActions/AddTypeSig.hs b/src/StaticLS/IDE/CodeActions/AddTypeSig.hs index 8e3bf0a7..5daec2e6 100644 --- a/src/StaticLS/IDE/CodeActions/AddTypeSig.hs +++ b/src/StaticLS/IDE/CodeActions/AddTypeSig.hs @@ -26,25 +26,25 @@ import StaticLS.Logger import StaticLS.Monad import StaticLS.Utils (isRightOrThrowT) -type AddTypeContext = Haskell.Bind :+ Haskell.Function :+ Nil +type AddTypeContext = Haskell.BindP :+ Haskell.FunctionP :+ Nil -type BindName = Haskell.PrefixId :+ Haskell.Variable :+ Nil +type BindName = Haskell.PrefixIdP :+ Haskell.VariableP :+ Nil -- For now, it only works with top level declarations -getDeclarationNameAtPos :: Haskell.Haskell -> Pos -> LineCol -> AST.Err (Maybe BindName) +getDeclarationNameAtPos :: Haskell.HaskellP -> Pos -> LineCol -> AST.Err (Maybe BindName) getDeclarationNameAtPos haskell pos _lineCol = do let node = AST.getDeepestContaining @AddTypeContext (Range.point pos) haskell.dynNode case node of Just bind | let dynNode = AST.getDynNode bind , (Just parent) <- dynNode.nodeParent - , Nothing <- AST.cast @Haskell.LocalBinds parent + , Nothing <- AST.cast @Haskell.LocalBindsP parent , let bindName = Monad.join $ Either.Extra.eitherToMaybe do case bind of - Inj (function :: Haskell.Function) -> do + Inj (function :: Haskell.FunctionP) -> do name <- AST.collapseErr function.name pure name - Inj @Haskell.Bind bind -> do + Inj @Haskell.BindP bind -> do name <- AST.collapseErr bind.name pure name _ -> Left "No Name found" diff --git a/src/StaticLS/IDE/CodeActions/AutoExport.hs b/src/StaticLS/IDE/CodeActions/AutoExport.hs index c42952d5..38fae8fa 100644 --- a/src/StaticLS/IDE/CodeActions/AutoExport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoExport.hs @@ -1,28 +1,14 @@ module StaticLS.IDE.CodeActions.AutoExport where import AST qualified -import AST.Haskell qualified import Data.Range (Range) import Data.Range qualified as Range -import StaticLS.Hir -import StaticLS.Hir qualified as Hir +import Hir +import Hir.Types qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.Monad -getDeclNode :: Hir.Decl -> AST.DynNode -getDeclNode decl = case decl of - DeclData d -> d.node.dynNode - DeclNewtype n -> n.node.dynNode - DeclClass c -> c.node.dynNode - DeclSig s -> s.node.dynNode - DeclBind b -> AST.getDynNode b.node - DeclDataFamily d -> d.node.dynNode - DeclPatternSig p -> p.node.dynNode - DeclPattern p -> p.node.dynNode - DeclTypeFamily t -> t.node.dynNode - DeclTypeSynonym t -> t.node.dynNode - -- declToExportItem :: Hir.Decl -> ExportItem -- declToExportItem decl = case decl of -- DeclData d -> ExportItem (getDeclName decl) (AST.getDynNode d.node) @@ -38,7 +24,7 @@ getDeclNode decl = case decl of getDeclarationsAtPoint :: Range -> [Hir.Decl] -> [Hir.Decl] getDeclarationsAtPoint range decls = - filter (\decl -> (getDeclNode decl).nodeRange `Range.containsRange` range) decls + filter (\decl -> (declDynNode decl).nodeRange `Range.containsRange` range) decls codeAction :: CodeActionContext -> StaticLsM [Assist] codeAction cx = do diff --git a/src/StaticLS/IDE/CodeActions/AutoImport.hs b/src/StaticLS/IDE/CodeActions/AutoImport.hs index 95213433..80c2df6f 100644 --- a/src/StaticLS/IDE/CodeActions/AutoImport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoImport.hs @@ -24,7 +24,8 @@ import Data.Text (Text) import Data.Text qualified as T import Database.SQLite.Simple import HieDb -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir +import Hir.Parse qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.IDE.SourceEdit (SourceEdit) @@ -134,7 +135,7 @@ data ImportInsertPoint = HeaderInsertPoint !Pos | AfterImportInsertPoint !Pos -getImportsInsertPoint :: Rope -> Haskell.Haskell -> AST.Err ImportInsertPoint +getImportsInsertPoint :: Rope -> Haskell.HaskellP -> AST.Err ImportInsertPoint getImportsInsertPoint _rope hs = do imports <- Tree.getImports hs header <- Tree.getHeader hs @@ -161,7 +162,7 @@ shouldAddNewline rope pos = do Just lineAfter -> not (T.all Char.isSpace lineAfter && T.elem '\n' lineAfter) Nothing -> True -insertImportChange :: H.Haskell -> Rope -> Text -> AST.Err Change +insertImportChange :: H.HaskellP -> Rope -> Text -> AST.Err Change insertImportChange tree rope toImport = do insertPoint <- getImportsInsertPoint rope tree pure $ case insertPoint of diff --git a/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs b/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs index cc35dd57..a7792c85 100644 --- a/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs +++ b/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs @@ -68,9 +68,9 @@ data PartialDeletionInfo = PartialDeletionInfo {} data FullDeletionInfo = FullDeletionInfo { path :: AbsPath , sourceRope :: Rope - , haskell :: Haskell.Haskell + , haskell :: Haskell.HaskellP , loc :: Range - , node :: Maybe Haskell.Import + , node :: Maybe Haskell.ImportP } mkDeletionInfo :: Diagnostic -> StaticLsM DeletionInfo @@ -87,7 +87,7 @@ mkFullDeletionInfo diagnostic = do node <- getImportAtLoc path loc pure FullDeletionInfo {..} -getImportAtLoc :: AbsPath -> Range -> StaticLsM (Maybe Haskell.Import) +getImportAtLoc :: AbsPath -> Range -> StaticLsM (Maybe Haskell.ImportP) getImportAtLoc path loc = do haskell <- getHaskell path let imports = getImports haskell @@ -154,7 +154,7 @@ createPartialDeletion :: DeletionInfo -> SourceEdit createPartialDeletion _diagnostic = do SourceEdit.empty -extend :: Rope -> Haskell.Haskell -> Range -> Range +extend :: Rope -> Haskell.HaskellP -> Range -> Range extend rope haskell _range@(Range start end) = do let newEnd = fromMaybe end $ lastPosOnLine rope end let nodePred node = if (AST.nodeRange node).start.pos > start.pos then Just node else Nothing diff --git a/src/StaticLS/IDE/Completion.hs b/src/StaticLS/IDE/Completion.hs index c83f94ab..39b91f0b 100644 --- a/src/StaticLS/IDE/Completion.hs +++ b/src/StaticLS/IDE/Completion.hs @@ -43,7 +43,7 @@ import HieDb (HieDb) import HieDb qualified import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.AllExtensions (allExtensions) import StaticLS.IDE.CodeActions.AutoImport qualified as IDE.CodeActions.AutoImport import StaticLS.IDE.Monad @@ -161,12 +161,12 @@ getModulePrefix cx sourceRope = do | firstIsUpper, Just (mod, match) <- TextUtils.splitOnceEnd "." prefix -> Just (mod, match) | otherwise -> Nothing -getImportPrefix :: Context -> Rope -> H.Haskell -> Maybe (Maybe Text) +getImportPrefix :: Context -> Rope -> H.HaskellP -> Maybe (Maybe Text) getImportPrefix cx sourceRope hs = do let lineCol = cx.lineCol let pos = cx.pos let line = Rope.toText $ Maybe.fromMaybe "" $ Rope.getLine sourceRope lineCol.line - let imports = AST.getDeepestContaining @Haskell.Imports (Range.point pos) (AST.getDynNode hs) + let imports = AST.getDeepestContaining @Haskell.ImportsP (Range.point pos) (AST.getDynNode hs) case "import" `T.stripPrefix` line of Just rest | Maybe.isJust imports -> do let mod = T.dropWhile Char.isSpace rest @@ -175,7 +175,7 @@ getImportPrefix cx sourceRope hs = do _ -> Nothing -- TODO: recognize headers properly -getLangextPrefix :: Context -> Rope -> H.Haskell -> Maybe Text +getLangextPrefix :: Context -> Rope -> H.HaskellP -> Maybe Text getLangextPrefix cx sourceRope hs = do let lineCol = cx.lineCol let pos = cx.pos @@ -183,7 +183,7 @@ getLangextPrefix cx sourceRope hs = do let line = Rope.toText $ Maybe.fromMaybe "" $ Rope.getLine sourceRope lineCol.line let (_rest, extPrefix) = Maybe.fromMaybe ("", "") $ TextUtils.splitOnceEnd " " line let dyn = AST.getDynNode hs - let pragma = AST.getDeepestContaining @Haskell.Pragma posRange dyn + let pragma = AST.getDeepestContaining @Haskell.PragmaP posRange dyn let isInPragma = Maybe.isJust pragma if isInPragma && extPrefix /= "" then Just extPrefix else Nothing diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 1a75094d..85bf2b5d 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -35,7 +35,8 @@ import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.Type qualified as HieView.Type import StaticLS.HieView.View qualified as HieView -import StaticLS.Hir qualified as Hir +import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.FileWith import StaticLS.IDE.FileWith qualified as FileWith import StaticLS.IDE.HiePos diff --git a/src/StaticLS/IDE/DocumentSymbols.hs b/src/StaticLS/IDE/DocumentSymbols.hs index 73d209e1..c19420fd 100644 --- a/src/StaticLS/IDE/DocumentSymbols.hs +++ b/src/StaticLS/IDE/DocumentSymbols.hs @@ -26,7 +26,7 @@ data SymbolTree = SymbolTree } deriving (Show) -queryDeclarations :: Haskell.Haskell -> AST.Err [Haskell.Declaration] +queryDeclarations :: Haskell.HaskellP -> AST.Err [Haskell.DeclarationP] queryDeclarations hs = do decls <- AST.collapseErr hs.declarations decls <- AST.maybeToErr "No declarations found" decls @@ -34,24 +34,24 @@ queryDeclarations hs = do let declarations = Maybe.mapMaybe ( \decl -> case decl of - Inj @Haskell.Declaration decl -> Just decl + Inj @Haskell.DeclarationP decl -> Just decl _ -> Nothing ) (NE.toList decls) pure declarations -declarationToSymbol :: Haskell.Declaration -> AST.Err [SymbolTree] +declarationToSymbol :: Haskell.DeclarationP -> AST.Err [SymbolTree] declarationToSymbol decl = case decl.getDeclaration of - Inj @Haskell.Decl decl -> declToSymbol decl - Inj @Haskell.DataType dataType -> dataTypeToSymbol dataType - Inj @Haskell.Newtype newtype_ -> newtypeToSymbol newtype_ - Inj @Haskell.Class class_ -> classToSymbol class_ - Inj @Haskell.TypeSynomym typeSynonym -> typeSynonymToSymbol typeSynonym - Inj @Haskell.TypeFamily typeFamily -> typeFamilyToSymbol typeFamily + Inj @Haskell.DeclP decl -> declToSymbol decl + Inj @Haskell.DataTypeP dataType -> dataTypeToSymbol dataType + Inj @Haskell.NewtypeP newtype_ -> newtypeToSymbol newtype_ + Inj @Haskell.ClassP class_ -> classToSymbol class_ + Inj @Haskell.TypeSynomymP typeSynonym -> typeSynonymToSymbol typeSynonym + Inj @Haskell.TypeFamilyP typeFamily -> typeFamilyToSymbol typeFamily _ -> pure [] -typeFamilyToSymbol :: Haskell.TypeFamily -> AST.Err [SymbolTree] +typeFamilyToSymbol :: Haskell.TypeFamilyP -> AST.Err [SymbolTree] typeFamilyToSymbol typeFamily = do name <- AST.collapseErr typeFamily.name pure $ Foldable.toList $ do @@ -63,7 +63,7 @@ typeFamilyToSymbol typeFamily = do (AST.nodeToRange typeFamily) (AST.nodeToRange name) -typeSynonymToSymbol :: Haskell.TypeSynomym -> AST.Err [SymbolTree] +typeSynonymToSymbol :: Haskell.TypeSynomymP -> AST.Err [SymbolTree] typeSynonymToSymbol typeSynonym = do name <- AST.collapseErr typeSynonym.name pure $ Foldable.toList $ do @@ -75,7 +75,7 @@ typeSynonymToSymbol typeSynonym = do (AST.nodeToRange typeSynonym) (AST.nodeToRange name) -newtypeToSymbol :: Haskell.Newtype -> AST.Err [SymbolTree] +newtypeToSymbol :: Haskell.NewtypeP -> AST.Err [SymbolTree] newtypeToSymbol newtype_ = do name <- AST.collapseErr newtype_.name pure $ Foldable.toList $ do @@ -87,7 +87,7 @@ newtypeToSymbol newtype_ = do (AST.nodeToRange newtype_) (AST.nodeToRange name) -classToSymbol :: Haskell.Class -> AST.Err [SymbolTree] +classToSymbol :: Haskell.ClassP -> AST.Err [SymbolTree] classToSymbol class_ = do name <- AST.collapseErr class_.name pure $ Foldable.toList $ do @@ -99,7 +99,7 @@ classToSymbol class_ = do (AST.nodeToRange class_) (AST.nodeToRange name) -dataTypeToSymbol :: Haskell.DataType -> AST.Err [SymbolTree] +dataTypeToSymbol :: Haskell.DataTypeP -> AST.Err [SymbolTree] dataTypeToSymbol dataType = do name <- AST.collapseErr dataType.name pure $ Foldable.toList $ do @@ -111,10 +111,10 @@ dataTypeToSymbol dataType = do (AST.nodeToRange dataType) (AST.nodeToRange name) -declToSymbol :: Haskell.Decl -> AST.Err [SymbolTree] +declToSymbol :: Haskell.DeclP -> AST.Err [SymbolTree] declToSymbol decl = case decl.getDecl of - Inj @Haskell.Bind bind -> do + Inj @Haskell.BindP bind -> do name <- AST.collapseErr bind.name pure $ Foldable.toList $ do name <- name @@ -124,7 +124,7 @@ declToSymbol decl = SymbolKind.Function (AST.nodeToRange decl) (AST.nodeToRange name) - Inj @Haskell.Function fun -> do + Inj @Haskell.FunctionP fun -> do name <- AST.collapseErr fun.name pure $ Foldable.toList $ do name <- name diff --git a/src/StaticLS/IDE/Hover.hs b/src/StaticLS/IDE/Hover.hs index 34b16982..77e819a9 100644 --- a/src/StaticLS/IDE/Hover.hs +++ b/src/StaticLS/IDE/Hover.hs @@ -39,7 +39,8 @@ import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.View qualified as HieView -import StaticLS.Hir qualified as Hir +import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.HiePos import StaticLS.IDE.Hover.Info import StaticLS.IDE.Monad @@ -105,7 +106,7 @@ retrieveHover path lineCol = do isInHoverName :: (MonadIde m) => AbsPath -> Range -> m Bool isInHoverName path range = do hs <- getHaskell path - let node = AST.getDeepestContaining @(H.Module AST.:+ Hir.ParseQualifiedTypes) range hs.dynNode + let node = AST.getDeepestContaining @(H.ModuleP AST.:+ Hir.ParseQualifiedTypes) range hs.dynNode pure $ Maybe.isJust node maxNames :: Int diff --git a/src/StaticLS/IDE/InlayHints/TypeAnnotations.hs b/src/StaticLS/IDE/InlayHints/TypeAnnotations.hs index ad6308c3..24904fbd 100644 --- a/src/StaticLS/IDE/InlayHints/TypeAnnotations.hs +++ b/src/StaticLS/IDE/InlayHints/TypeAnnotations.hs @@ -60,7 +60,7 @@ nodeIsVarAtBinding :: ASTLoc -> Bool nodeIsVarAtBinding astLoc = isJust $ do let curNode = nodeAtLoc astLoc let nameCorrect = maybe False (`elem` ["name", "pattern", "element", "left_operand", "right_operand"]) curNode.nodeFieldName - let typeCorrect = isJust (cast @Haskell.Variable curNode) || isJust (cast @Haskell.Function curNode) + let typeCorrect = isJust (cast @Haskell.VariableP curNode) || isJust (cast @Haskell.FunctionP curNode) let headNodeGood = nameCorrect && typeCorrect let criterion = nthChildOf 0 (\y -> isBind y || isAlt y || isFunction y) guard headNodeGood @@ -72,10 +72,10 @@ nodeIsVarAtBinding astLoc = isJust $ do nodeIsRecordVar :: ASTLoc -> Bool nodeIsRecordVar astLoc = isJust $ do let curNode = nodeAtLoc astLoc - _ <- cast @Haskell.Variable curNode + _ <- cast @Haskell.VariableP curNode let name = curNode.nodeFieldName let isBound = maybe False (`elem` ["pattern", "element", "left_operand", "right_operand"]) name - fpParent <- findAncestor (isJust . cast @Haskell.FieldPattern . nodeAtLoc) astLoc + fpParent <- findAncestor (isJust . cast @Haskell.FieldPatternP . nodeAtLoc) astLoc let fpChildren = children fpParent case length fpChildren of 1 -> Applicative.empty @@ -84,26 +84,26 @@ nodeIsRecordVar astLoc = isJust $ do nodeIsUpdatedField :: ASTLoc -> Bool nodeIsUpdatedField astLoc = isJust $ do let curNode = nodeAtLoc astLoc - _ <- cast @Haskell.Variable curNode + _ <- cast @Haskell.VariableP curNode -- let name = curNode.nodeFieldName -- let isBound = maybe False (`elem` ["pattern", "element", "left_operand", "right_operand"]) name -- let isPun = name == Nothing - fieldNode <- findAncestor (isJust . cast @Haskell.FieldName . nodeAtLoc) astLoc + fieldNode <- findAncestor (isJust . cast @Haskell.FieldNameP . nodeAtLoc) astLoc guard $ childIndex fieldNode == Just 0 - _ <- findAncestor (isJust . cast @Haskell.FieldUpdate . nodeAtLoc) fieldNode + _ <- findAncestor (isJust . cast @Haskell.FieldUpdateP . nodeAtLoc) fieldNode pure () isAlt :: DynNode -> Bool -isAlt = isJust . cast @Haskell.Alternative +isAlt = isJust . cast @Haskell.AlternativeP isFunction :: DynNode -> Bool -isFunction = isJust . cast @Haskell.Function +isFunction = isJust . cast @Haskell.FunctionP isBind :: DynNode -> Bool -isBind = isJust . cast @Haskell.Bind +isBind = isJust . cast @Haskell.BindP isLet :: DynNode -> Bool -isLet = isJust . cast @Haskell.Let +isLet = isJust . cast @Haskell.LetP selectNodesToType :: StaticEnvOptions -> DynNode -> [DynNode] selectNodesToType options root = do diff --git a/src/StaticLS/IDE/InlayHints/Wildcard.hs b/src/StaticLS/IDE/InlayHints/Wildcard.hs index 97054b64..409d8201 100644 --- a/src/StaticLS/IDE/InlayHints/Wildcard.hs +++ b/src/StaticLS/IDE/InlayHints/Wildcard.hs @@ -32,7 +32,7 @@ import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.View qualified as HieView -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.HiePos import StaticLS.IDE.Hover.Info import StaticLS.IDE.InlayHints.Common @@ -49,16 +49,16 @@ getInlayHints absPath = do inlayHints <- catMaybes <$> traverse (mkInlayHint absPath rope) dynNodesToType pure inlayHints -selectNodesToAnn :: Haskell.Haskell -> [Haskell.Wildcard] +selectNodesToAnn :: Haskell.HaskellP -> [Haskell.WildcardP] selectNodesToAnn haskell = do let astLocs = leaves $ rootToASTLoc $ getDynNode haskell [ wildcard | astLoc <- astLocs - , Just wildcard <- [cast @Haskell.Wildcard (nodeAtLoc astLoc)] + , Just wildcard <- [cast @Haskell.WildcardP (nodeAtLoc astLoc)] , Just _ <- [parent astLoc] ] -mkInlayHint :: AbsPath -> Rope.Rope -> Haskell.Wildcard -> StaticLsM (Maybe InlayHint) +mkInlayHint :: AbsPath -> Rope.Rope -> Haskell.WildcardP -> StaticLsM (Maybe InlayHint) mkInlayHint absPath rope wildcard = do let lcr = nodeToRange wildcard h <- retrieveHover absPath $ posToLineCol rope lcr.start @@ -118,7 +118,7 @@ dedup lines = do isInHoverName :: (MonadIde m) => AbsPath -> Range -> m Bool isInHoverName path range = do hs <- getHaskell path - let node = AST.getDeepestContaining @(H.Module AST.:+ Hir.ParseQualifiedTypes) range hs.dynNode + let node = AST.getDeepestContaining @(H.ModuleP AST.:+ Hir.ParseQualifiedTypes) range hs.dynNode pure $ Maybe.isJust node docsAtPoint :: (MonadIde m) => AbsPath -> HieView.File -> Pos -> LineCol -> m [NameDocs] diff --git a/src/StaticLS/IDE/Monad.hs b/src/StaticLS/IDE/Monad.hs index 74d5c158..520cbf1d 100644 --- a/src/StaticLS/IDE/Monad.hs +++ b/src/StaticLS/IDE/Monad.hs @@ -63,7 +63,8 @@ import Data.Time import StaticLS.FilePath import StaticLS.HIE.File qualified as HIE.File import StaticLS.HieView qualified as HieView -import StaticLS.Hir qualified as Hir +import Hir qualified as Hir +import Hir.Types qualified as Hir import StaticLS.Logger import StaticLS.PositionDiff qualified as PositionDiff import StaticLS.Semantic @@ -133,7 +134,7 @@ getFileState path = do ) env.fileStateCache -getHaskell :: (MonadIde m, MonadIO m) => AbsPath -> m Haskell.Haskell +getHaskell :: (MonadIde m, MonadIO m) => AbsPath -> m Haskell.HaskellP getHaskell path = do fileState <- getFileState path pure fileState.tree diff --git a/src/StaticLS/IDE/References.hs b/src/StaticLS/IDE/References.hs index 4bd1e376..4477661e 100644 --- a/src/StaticLS/IDE/References.hs +++ b/src/StaticLS/IDE/References.hs @@ -25,7 +25,8 @@ import StaticLS.HIE.File hiding (getHieSource) import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir +import Hir.Parse qualified as Hir import StaticLS.IDE.FileWith (FileLcRange, FileRange, FileWith' (..)) import StaticLS.IDE.HiePos import StaticLS.IDE.Monad diff --git a/src/StaticLS/IDE/Rename.hs b/src/StaticLS/IDE/Rename.hs index f2c3fd1d..80d6f3a3 100644 --- a/src/StaticLS/IDE/Rename.hs +++ b/src/StaticLS/IDE/Rename.hs @@ -22,7 +22,8 @@ import Data.Range qualified as Range import Data.Text (Text) import Data.Text qualified as T import Data.Traversable (for) -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir +import Hir.Parse qualified as Hir import StaticLS.IDE.FileWith (FileWith' (..)) import StaticLS.IDE.Monad import StaticLS.IDE.References qualified as References @@ -33,16 +34,16 @@ import StaticLS.Monad data RenameContext = RenameQualified Hir.Qualified - | RenameTopSplice H.TopSplice - | RenameSplice H.Splice + | RenameTopSplice H.TopSpliceP + | RenameSplice H.SpliceP | RenameOther deriving (Show) -getRenameContext :: H.Haskell -> Range -> AST.Err RenameContext +getRenameContext :: H.HaskellP -> Range -> AST.Err RenameContext getRenameContext hs range = do let nameTypes = Hir.getNameTypes range hs - let topSplice = AST.getDeepestContaining @H.TopSplice range hs.dynNode - let splice = AST.getDeepestContaining @H.Splice range hs.dynNode + let topSplice = AST.getDeepestContaining @H.TopSpliceP range hs.dynNode + let splice = AST.getDeepestContaining @H.SpliceP range hs.dynNode qualified <- Hir.getQualifiedAtPoint range hs let res = (RenameOther <$ nameTypes) @@ -62,7 +63,7 @@ getEverything node = case AST.cast node of renameSplice :: DynNode -> Text -> Text -> [Change] renameSplice node old new = do - let everything = traverse Hir.parseThQuotedName (getEverything @H.ThQuotedName node) + let everything = traverse Hir.parseThQuotedName (getEverything @H.ThQuotedNameP node) case everything of Left _e -> do [] diff --git a/src/StaticLS/IDE/Utils.hs b/src/StaticLS/IDE/Utils.hs index 99338fca..34e8a4d7 100644 --- a/src/StaticLS/IDE/Utils.hs +++ b/src/StaticLS/IDE/Utils.hs @@ -5,7 +5,8 @@ import Control.Monad import Data.Path (AbsPath) import Data.Path qualified as Path import Data.Text qualified as T -import StaticLS.Hir qualified as Hir +import Hir.Types qualified as Hir +import Hir.Parse qualified as Hir import StaticLS.Monad import StaticLS.StaticEnv import System.FilePath diff --git a/src/StaticLS/Semantic.hs b/src/StaticLS/Semantic.hs index 6ea590ca..35b2124f 100644 --- a/src/StaticLS/Semantic.hs +++ b/src/StaticLS/Semantic.hs @@ -8,14 +8,15 @@ import Data.Rope (Rope) import Data.Rope qualified as Rope import Data.Text (Text) import Data.Text qualified as T -import StaticLS.Hir qualified as Hir +import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.PositionDiff qualified as PositionDiff -- keep these fields lazy data FileState = FileState { contentsRope :: Rope , contentsText :: Text - , tree :: Haskell.Haskell + , tree :: Haskell.HaskellP , hir :: Hir.Program , hirParseErrors :: [Text] , tokens :: [PositionDiff.Token] diff --git a/src/StaticLS/Tree.hs b/src/StaticLS/Tree.hs index f0df3060..4a33c557 100644 --- a/src/StaticLS/Tree.hs +++ b/src/StaticLS/Tree.hs @@ -22,7 +22,7 @@ import Data.Rope (Rope) byteToPos :: Rope -> Int -> Pos byteToPos _rope byte = Pos byte -getHeader :: Haskell.Haskell -> AST.Err (Maybe Haskell.Header) +getHeader :: Haskell.HaskellP -> AST.Err (Maybe Haskell.HeaderP) getHeader haskell = do header <- AST.collapseErr haskell.children pure header @@ -31,13 +31,13 @@ getHeader haskell = do data Imports = Imports { dynNode :: AST.DynNode - , imports :: [Haskell.Import] + , imports :: [Haskell.ImportP] } filterErr :: (Foldable f) => f (AST.Err a) -> [a] filterErr = Maybe.mapMaybe Either.Extra.eitherToMaybe . Foldable.toList -getImports :: Haskell.Haskell -> AST.Err (Maybe Imports) +getImports :: Haskell.HaskellP -> AST.Err (Maybe Imports) getImports hs = do imports <- AST.collapseErr hs.imports case imports of @@ -57,7 +57,7 @@ data Qualified = Qualified , id :: AST.DynNode } -parseQualified :: Haskell.Qualified -> AST.Err Qualified +parseQualified :: Haskell.QualifiedP -> AST.Err Qualified parseQualified qualified = do module' <- qualified.module' modIds <- AST.collapseErr module'.children @@ -66,9 +66,9 @@ parseQualified qualified = do id <- pure $ AST.getDynNode id pure Qualified {modIds, id} -getQualifiedAtPoint :: Haskell.Haskell -> Range -> AST.Err (Maybe Qualified) +getQualifiedAtPoint :: Haskell.HaskellP -> Range -> AST.Err (Maybe Qualified) getQualifiedAtPoint hs pos = do - let node = AST.getDeepestContaining @Haskell.Qualified pos (AST.getDynNode hs) + let node = AST.getDeepestContaining @Haskell.QualifiedP pos (AST.getDynNode hs) case node of Nothing -> pure Nothing Just node -> do diff --git a/static-ls.cabal b/static-ls.cabal index 5d47cdd1..ed140b25 100644 --- a/static-ls.cabal +++ b/static-ls.cabal @@ -66,6 +66,7 @@ library , ghc >=9.4.3 && <9.11 , ghc-paths >=0.1.0 && <0.2 , hashable + , haskell-arborist , haskell-ast , haskell-lexer >=1.1.1 && <1.2.0 , hiedb ==0.6.* @@ -104,7 +105,6 @@ library Data.Diff Data.Edit Data.ListUtils - Data.Path Data.RangeMap Data.Rope Data.TextUtils @@ -124,12 +124,6 @@ library StaticLS.HieView.Type StaticLS.HieView.Utils StaticLS.HieView.View - StaticLS.Hir - StaticLS.Hir.Make - StaticLS.Hir.Name - StaticLS.Hir.Parse - StaticLS.Hir.Print - StaticLS.Hir.Types StaticLS.IDE.AllExtensions StaticLS.IDE.CodeActions StaticLS.IDE.CodeActions.AddTypeSig @@ -219,6 +213,7 @@ executable print-hie , ghc >=9.4.3 && <9.11 , ghc-paths >=0.1.0 && <0.2 , hashable + , haskell-arborist , haskell-ast , haskell-lexer >=1.1.1 && <1.2.0 , hiedb ==0.6.* @@ -288,6 +283,7 @@ executable static-ls , ghc >=9.4.3 && <9.11 , ghc-paths >=0.1.0 && <0.2 , hashable + , haskell-arborist , haskell-ast , haskell-lexer >=1.1.1 && <1.2.0 , hiedb ==0.6.* @@ -373,6 +369,7 @@ test-suite expect_tests , ghc >=9.4.3 && <9.11 , ghc-paths >=0.1.0 && <0.2 , hashable + , haskell-arborist , haskell-ast , haskell-lexer >=1.1.1 && <1.2.0 , hiedb ==0.6.* @@ -447,6 +444,7 @@ test-suite static-ls-test , ghc >=9.4.3 && <9.11 , ghc-paths >=0.1.0 && <0.2 , hashable + , haskell-arborist , haskell-ast , haskell-lexer >=1.1.1 && <1.2.0 , hiedb ==0.6.* diff --git a/test/Semantic/HirSpec.hs b/test/Semantic/HirSpec.hs index 9f4b9573..d1caacb6 100644 --- a/test/Semantic/HirSpec.hs +++ b/test/Semantic/HirSpec.hs @@ -4,7 +4,7 @@ module Semantic.HirSpec where import AST.Haskell qualified import NeatInterpolation -import StaticLS.Hir qualified as Hir +import Hir.Parse qualified as Hir import Test.Hspec spec :: Spec diff --git a/test/StaticLS/HirSpec.hs b/test/StaticLS/HirSpec.hs index a5980584..d1df0c09 100644 --- a/test/StaticLS/HirSpec.hs +++ b/test/StaticLS/HirSpec.hs @@ -9,7 +9,7 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.Read qualified as T.Read import NeatInterpolation (trimming) -import StaticLS.Hir qualified as Hir +import Hir.Parse qualified as Hir import StaticLS.Utils (isJustOrThrowS, isRightOrThrowS) import Test.Hspec import TestImport.Annotation qualified as Annotation diff --git a/test/StaticLS/IDE/RenameSpec.hs b/test/StaticLS/IDE/RenameSpec.hs index 6bf0f5bb..1873bdb5 100644 --- a/test/StaticLS/IDE/RenameSpec.hs +++ b/test/StaticLS/IDE/RenameSpec.hs @@ -19,7 +19,7 @@ spec :: Spec spec = do let check name source oldName newName expected = it name do let hs = Haskell.parse source - let splice = head $ getEverything @Haskell.TopSplice hs.dynNode + let splice = head $ getEverything @Haskell.TopSpliceP hs.dynNode let changes = renameSplice splice.dynNode oldName newName let sourceRope = Rope.fromText source let newSourceRope = Rope.edit (Edit.changesToEdit changes) sourceRope From a301c9b3903300b114e3c6b2c49185f51fd23304 Mon Sep 17 00:00:00 2001 From: Joseph Sumabat Date: Fri, 18 Apr 2025 14:23:22 -0400 Subject: [PATCH 02/21] hover and def --- cabal.project | 12 +- expect_tests/HirTest.hs | 4 +- expect_tests/Main.hs | 2 +- nix/overlays/default.nix | 8 +- package.yaml | 2 + src/StaticLS/Arborist.hs | 130 +++++++++++++++++++++ src/StaticLS/Handlers.hs | 3 +- src/StaticLS/IDE/CodeActions/AutoImport.hs | 2 +- src/StaticLS/IDE/Completion.hs | 2 +- src/StaticLS/IDE/Definition.hs | 117 ++++++------------- src/StaticLS/IDE/Hover.hs | 20 ++-- src/StaticLS/IDE/Hover/Info.hs | 2 + src/StaticLS/IDE/InlayHints/Wildcard.hs | 2 +- src/StaticLS/IDE/Monad.hs | 46 +++++++- src/StaticLS/IDE/References.hs | 4 +- src/StaticLS/IDE/Rename.hs | 2 +- src/StaticLS/IDE/Utils.hs | 2 +- src/StaticLS/Monad.hs | 4 +- static-ls.cabal | 6 + test/Semantic/HirSpec.hs | 2 +- test/StaticLS/HirSpec.hs | 2 +- 21 files changed, 255 insertions(+), 119 deletions(-) create mode 100644 src/StaticLS/Arborist.hs diff --git a/cabal.project b/cabal.project index 73879fb2..cf55a839 100644 --- a/cabal.project +++ b/cabal.project @@ -8,37 +8,37 @@ packages: source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e + tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea subdir: tree-sitter-simple source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e + tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea subdir: tree-sitter-haskell source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e + tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea subdir: tree-sitter-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e + tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea subdir: haskell-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 03085cb9e6f1c7850e0c3bcd227ba0c730c7013e + tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea subdir: text-range source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: d69de64fcf2cd729816b18f1a0a410c198de62af + tag: 6b1b2ae2edf5d69a1760d4cb0bee72eea267c1a8 source-repository-package type: git diff --git a/expect_tests/HirTest.hs b/expect_tests/HirTest.hs index 070004f9..6080c88b 100644 --- a/expect_tests/HirTest.hs +++ b/expect_tests/HirTest.hs @@ -5,9 +5,9 @@ module HirTest where import AST.Haskell qualified as H import Data.Text qualified as T import Data.Text.Lazy qualified as TL -import NeatInterpolation -import Hir.Types qualified as Hir import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir +import NeatInterpolation import Test.Tasty import Test.Tasty.Expect import Text.Pretty.Simple qualified as Pretty diff --git a/expect_tests/Main.hs b/expect_tests/Main.hs index aac48bc8..07f46885 100644 --- a/expect_tests/Main.hs +++ b/expect_tests/Main.hs @@ -6,7 +6,7 @@ import Test.Tasty.Expect main :: IO () main = do - --defaultMainWithIngredients (expectIngredient : defaultIngredients) tests + -- defaultMainWithIngredients (expectIngredient : defaultIngredients) tests pure () tests :: TestTree diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 3a1ffc1a..0b2c5287 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -2,15 +2,15 @@ let tree-sitter-simple-repo = { url = "https://github.com/josephsumabat/tree-sitter-simple"; - sha256 = "sha256-X4SM6v9CMs3DeX7thhV+8bEmVxDNdMGaqHr9t0dDdYw="; - rev = "03085cb9e6f1c7850e0c3bcd227ba0c730c7013e"; + sha256 = "sha256-un4lP1i/Xr4BnzHvyvFOlPs6kPqRA8tVwFZrQNY0+z0="; + rev = "9ba9cdf3f678b1f5e68258247c33f6788bca6cea"; fetchSubmodules = true; }; haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-7KhlcDsrdxHwEl3FCa6WHneP7En6Luqfd9mUQw+BrP4="; - rev = "d69de64fcf2cd729816b18f1a0a410c198de62af"; + sha256 = "sha256-4ByyZC4Uu39f/6LnPUQzt8hDZ+ypjz2/726Wlqq6XVQ="; + rev = "6b1b2ae2edf5d69a1760d4cb0bee72eea267c1a8"; fetchSubmodules = true; }; diff --git a/package.yaml b/package.yaml index 9c22fd3a..b3330904 100644 --- a/package.yaml +++ b/package.yaml @@ -69,6 +69,8 @@ dependencies: - regex-tdfa # why is the api for this library so bad - stm - haskell-arborist + - nonempty-containers + language: GHC2021 default-extensions: - LambdaCase diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs new file mode 100644 index 00000000..4b01ab43 --- /dev/null +++ b/src/StaticLS/Arborist.hs @@ -0,0 +1,130 @@ +module StaticLS.Arborist where + +import AST qualified +import AST.Haskell qualified as H +import Arborist.Files +import Arborist.ModGraph +import Arborist.Renamer +import Arborist.Scope.Types +import Control.Error +import Control.Monad qualified as Monad +import Control.Monad.IO.Class +import Data.HashMap.Lazy qualified as HashMap +import Data.HashMap.Lazy qualified as Map +import Data.LineCol (LineCol (..)) +import Data.LineColRange +import Data.List qualified as List +import Data.List.NonEmpty qualified as NE +import Data.Path qualified as Path +import Data.Set.NonEmpty qualified as NESet +import Data.Text +import Data.Text qualified as T +import Data.Time +import Debug.Trace +import Hir.Types qualified as Hir +import Language.LSP.Protocol.Types +import StaticLS.IDE.FileWith +import StaticLS.IDE.Monad +import StaticLS.ProtoLSP qualified as ProtoLSP +import System.Directory (doesFileExist) +import Arborist.Haddock + +time :: (MonadIO m) => [Char] -> m a -> m a +time label fn = do + start <- liftIO getCurrentTime + res <- fn + end <- liftIO getCurrentTime + traceShowM $ "Time to run " <> label <> ": " ++ show (diffUTCTime end start) + pure res + +getResolvedVar :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase)) +getResolvedVar target lc = fst <$> getResolvedVarAndPrgs target lc + +getResolvedVarAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase), ProgramIndex) +getResolvedVarAndPrgs target lc = do + time "resolvedVar" $ do + modFileMap <- getModFileMap + prgIndex <- getPrgIndex + (requiredPrograms) <- liftIO $ time "gather" $ gatherScopeDeps prgIndex target modFileMap (Just 2) + tryWritePrgIndex (\_ -> requiredPrograms) + let renameTree = renamePrg requiredPrograms HashMap.empty target + let resolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree + pure (resolvedVar, requiredPrograms) + + +getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo +getRequiredHaddock prgIndex varInfo = + let mPrg = Map.lookup varInfo.originatingMod prgIndex + haddockIndex = maybe Map.empty (indexPrgHaddocks Map.empty) mPrg + qualName = glblVarInfoToQualified varInfo + in + Map.lookup qualName haddockIndex + +------------------- +-- Definition +------------------- +modLocToFileLcRange :: (MonadIO m) => ModFileMap -> (Hir.ModuleText, LineColRange) -> m [FileLcRange] +modLocToFileLcRange modFileMap (modText, lcRange) = do + let paths = maybe [] List.singleton (Map.lookup modText modFileMap) + pathList <- fmap catMaybes $ + Monad.forM paths $ \path -> do + fileExists <- liftIO $ doesFileExist path + if fileExists + then do + absPath <- Path.filePathToAbs path + pure $ + Just $ + FileWith + { path = absPath + , loc = lcRange + } + else pure Nothing + pure pathList + +varToFileLcRange :: (MonadIO m) => ModFileMap -> Hir.ModuleText -> (H.Variable RenamePhase) -> m [FileLcRange] +varToFileLcRange modFileMap thisMod varNode = do + let locLst = maybe [] (resolvedLocs thisMod) varNode.ext + fileLcRanges <- Monad.join <$> (mapM (modLocToFileLcRange modFileMap) locLst) + pure fileLcRanges + +------------------- +-- Hover +------------------- + +varToHover :: ProgramIndex -> (H.Variable RenamePhase) -> Maybe Hover +varToHover prgIndex varNode = + let mResolvedVar = varNode.ext + range = ProtoLSP.lineColRangeToProto varNode.dynNode.nodeLineColRange + mContents = (resolvedVarToContents prgIndex =<< mResolvedVar) + in (\contents -> Hover + { _range = Just range + , _contents = InL $ MarkupContent MarkupKind_Markdown contents + }) <$> mContents + +resolvedVarToContents :: ProgramIndex -> ResolvedVariable -> Maybe Text +resolvedVarToContents prgIndex resolvedVar = + case resolvedVar of + ResolvedVariable (ResolvedGlobal glblVarInfo) -> + let mHover = getRequiredHaddock prgIndex glblVarInfo in + Just $ renderGlblVarInfo mHover glblVarInfo + _ -> Nothing + +renderGlblVarInfo :: Maybe HaddockInfo -> GlblVarInfo -> Text +renderGlblVarInfo mHaddock glblVarInfo = + wrapHaskell + (T.intercalate "\n" + [haddock + ,tySig + ] + ) + <> " \n\nimported from: *" + <> T.intercalate ", " (NE.toList $ (.mod.text) <$> NESet.toList glblVarInfo.importedFrom) + <> "*" + <> " \noriginates from: *" + <> glblVarInfo.originatingMod.text <> "*" + where + haddock = + maybe "" (.text) mHaddock + tySig = + maybe "" (.node.dynNode.nodeText) glblVarInfo.sig + wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" diff --git a/src/StaticLS/Handlers.hs b/src/StaticLS/Handlers.hs index e99a88a9..82b3c012 100644 --- a/src/StaticLS/Handlers.hs +++ b/src/StaticLS/Handlers.hs @@ -25,6 +25,7 @@ import Language.LSP.Server ( ) import Language.LSP.Server qualified as LSP import Language.LSP.VFS (VirtualFile (..)) +import StaticLS.Arborist import StaticLS.GhcidSession import StaticLS.HIE.File qualified as HIE.File import StaticLS.IDE.CodeActions (getCodeActions) @@ -69,7 +70,7 @@ handleTextDocumentHoverRequest :: Handlers (LspT c StaticLsM) handleTextDocumentHoverRequest = LSP.requestHandler LSP.SMethod_TextDocumentHover $ \req resp -> do let hoverParams = req._params path <- ProtoLSP.tdiToAbsPath hoverParams._textDocument - hover <- lift $ retrieveHover path (ProtoLSP.lineColFromProto hoverParams._position) + hover <- lift $ time "hover" $ retrieveHover path (ProtoLSP.lineColFromProto hoverParams._position) resp $ Right $ maybeToNull hover handleDefinitionRequest :: Handlers (LspT c StaticLsM) diff --git a/src/StaticLS/IDE/CodeActions/AutoImport.hs b/src/StaticLS/IDE/CodeActions/AutoImport.hs index 80c2df6f..86443689 100644 --- a/src/StaticLS/IDE/CodeActions/AutoImport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoImport.hs @@ -24,8 +24,8 @@ import Data.Text (Text) import Data.Text qualified as T import Database.SQLite.Simple import HieDb -import Hir.Types qualified as Hir import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.IDE.SourceEdit (SourceEdit) diff --git a/src/StaticLS/IDE/Completion.hs b/src/StaticLS/IDE/Completion.hs index 39b91f0b..4e131611 100644 --- a/src/StaticLS/IDE/Completion.hs +++ b/src/StaticLS/IDE/Completion.hs @@ -41,9 +41,9 @@ import Database.SQLite.Simple qualified as SQL import GHC.Generics (Generic) import HieDb (HieDb) import HieDb qualified +import Hir.Types qualified as Hir import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query -import Hir.Types qualified as Hir import StaticLS.IDE.AllExtensions (allExtensions) import StaticLS.IDE.CodeActions.AutoImport qualified as IDE.CodeActions.AutoImport import StaticLS.IDE.Monad diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 85bf2b5d..4fd358ce 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -2,10 +2,10 @@ module StaticLS.IDE.Definition ( getDefinition, getTypeDefinition, nameToLocation, - findDefString, ) where import AST qualified +import Control.Applicative import Control.Error import Control.Monad qualified as Monad import Control.Monad.Extra (mapMaybeM) @@ -28,6 +28,9 @@ import GHC qualified import GHC.Types.Name qualified as GHC import HieDb (HieDb) import HieDb qualified +import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir +import StaticLS.Arborist import StaticLS.FilePath import StaticLS.HIE.File import StaticLS.HIE.Position @@ -35,8 +38,6 @@ import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.Type qualified as HieView.Type import StaticLS.HieView.View qualified as HieView -import Hir.Parse qualified as Hir -import Hir.Types qualified as Hir import StaticLS.IDE.FileWith import StaticLS.IDE.FileWith qualified as FileWith import StaticLS.IDE.HiePos @@ -52,6 +53,34 @@ getDefinition :: LineCol -> m [FileLcRange] getDefinition path lineCol = do + hieDef <- getHieDefinition path lineCol + arboristDef <- getArboristDefinition path lineCol + pure $ + case hieDef of + [] -> arboristDef + _ -> hieDef + +getArboristDefinition :: + (MonadIde m, MonadIO m) => + AbsPath -> + LineCol -> + m [FileLcRange] +getArboristDefinition path lineCol = do + modFileMap <- getModFileMap + prg <- getHir path + mVarNode <- getResolvedVar prg lineCol + let mResult = do + varNode <- mVarNode + modText <- prg.mod + pure $ varToFileLcRange modFileMap modText varNode + fromMaybe (pure []) mResult + +getHieDefinition :: + (MonadIde m, MonadIO m) => + AbsPath -> + LineCol -> + m [FileLcRange] +getHieDefinition path lineCol = do pos <- lineColToPos path lineCol throwIfInThSplice "getDefinition" path pos hs <- getHaskell path @@ -70,12 +99,7 @@ getDefinition path lineCol = do let identifiers = HieView.Query.fileIdentifiersAtRangeList (Just (LineColRange.point hieLineCol)) hieView pure identifiers identifiers <- pure $ Maybe.fromMaybe [] identifiers - fileLcs <- case qual of - Right (Just qual) | null identifiers -> do - logInfo "no identifiers under cursor found, fallback logic" - res <- findDefString qual - hieFileLcToFileLcParallel res - _ -> do + fileLcs <- do mLocationLinks <- do locations <- traverse identifierToLocation identifiers locations <- pure $ concat locations @@ -143,9 +167,9 @@ nameToLocation name = fmap (fromMaybe []) <$> runMaybeT $ do let absRange = FileWith.mapPath (staticEnv.wsRoot Path.) range pure [absRange] Nothing -> - fallbackToDb - | otherwise -> fallbackToDb - Nothing -> fallbackToDb + MaybeT $ pure Nothing + | otherwise -> MaybeT $ pure Nothing + Nothing -> MaybeT $ pure Nothing where modSrcFile name = do staticEnv <- getStaticEnv @@ -160,75 +184,6 @@ nameToLocation name = fmap (fromMaybe []) <$> runMaybeT $ do exists <- doesFileExist (Path.toFilePath fileRange.path) return $ if exists then Just fileRange else Nothing - fallbackToDb :: (HasCallStack, HasStaticEnv m, MonadIO m, HasLogger m) => MaybeT m [FileLcRange] - fallbackToDb = do - -- This case usually arises when the definition is in an external package. - -- In this case the interface files contain garbage source spans - -- so we instead read the .hie files to get useful source spans. - logInfo "fallbackToDb" - erow <- runHieDbMaybeT (\hieDb -> hieDbFindDef hieDb name (HieView.Name.getUnit name)) - case erow of - [] -> do - logInfo "trying again without unit id" - -- If the lookup failed, try again without specifying a unit-id. - -- This is a hack to make find definition work better with ghcide's nascent multi-component support, - -- where names from a component that has been indexed in a previous session but not loaded in this - -- session may end up with different unit ids - erow' <- runHieDbMaybeT (\hieDb -> hieDbFindDef hieDb name Nothing) - case erow' of - [] -> MaybeT $ pure Nothing - xs -> lift $ mapMaybeM (runMaybeT . defRowToLocation) xs - xs -> lift $ mapMaybeM (runMaybeT . defRowToLocation) xs - -hieDbFindDef :: HieDb -> HieView.Name.Name -> Maybe Text -> IO [HieDb.DefRow] -hieDbFindDef conn name unit = - SQL.queryNamed - (HieDb.getConn conn) - "SELECT defs.* \ - \FROM defs JOIN mods USING (hieFile) \ - \WHERE occ = :occ AND (:mod IS NULL OR mod = :mod) AND (:unit IS NULL OR unit = :unit)" - [ ":occ" SQL.:= HieView.Name.toGHCOccName name - , ":mod" SQL.:= HieView.Name.getModule name - , ":unit" SQL.:= unit - ] - -findDefString :: - (MonadIde m, MonadIO m) => - Hir.Qualified -> - m [FileLcRange] -findDefString qual = do - let name = qual.name.node.nodeText - let mod = qual.mod - res <- runMaybeT do - res <- hieDbFindDefString name mod - mapMaybeM (runMaybeT . defRowToLocation) res - pure $ Maybe.fromMaybe [] res - -hieDbFindDefString :: (HasStaticEnv m, MonadIO m) => Text -> Maybe Hir.ModuleName -> MaybeT m [HieDb.DefRow] -hieDbFindDefString name mod = do - -- we need to resolve the mod first - let _modText = (.mod.text) <$> mod - runHieDbMaybeT - ( \hieDb -> - SQL.queryNamed - (HieDb.getConn hieDb) - "SELECT defs.* \ - \FROM defs JOIN mods USING (hieFile) \ - \WHERE occ LIKE :occ" - [ ":occ" SQL.:= ("_:" <> name) - ] - ) - -defRowToLocation :: (HasCallStack, HasLogger m, HasStaticEnv m, MonadIO m) => HieDb.DefRow -> MaybeT m FileLcRange -defRowToLocation defRow = do - let start = hiedbCoordsToLineCol (defRow.defSLine, defRow.defSCol) - end = hiedbCoordsToLineCol (defRow.defELine, defRow.defECol) - range = LineColRange start end - hieFilePath = defRow.defSrc - hieFilePath <- Path.filePathToAbs hieFilePath - file <- hieFilePathToSrcFilePath hieFilePath - pure $ FileWith file range - persistentModelNameToFileLc :: (MonadIde m, MonadIO m) => Text -> m (Maybe FileLcRange) persistentModelNameToFileLc persistentModelName = do staticEnv <- getStaticEnv diff --git a/src/StaticLS/IDE/Hover.hs b/src/StaticLS/IDE/Hover.hs index 77e819a9..f122b56a 100644 --- a/src/StaticLS/IDE/Hover.hs +++ b/src/StaticLS/IDE/Hover.hs @@ -1,7 +1,4 @@ -module StaticLS.IDE.Hover ( - retrieveHover, -) -where +module StaticLS.IDE.Hover where import Control.Monad.IO.Class import Control.Monad.RWS @@ -25,22 +22,23 @@ import Language.LSP.Protocol.Types ( ) import StaticLS.HI import StaticLS.HI.File +import TreeSitter.Api import AST qualified import AST.Haskell qualified as H +import Control.Applicative import Control.Monad qualified as Monad import Data.LineColRange qualified as LineColRange import Data.Maybe qualified as Maybe import Data.Pos (Pos) -import Data.Range (Range) import Data.Range qualified as Range +import Hir.Types qualified as Hir import Language.LSP.Protocol.Types qualified as LSP +import StaticLS.Arborist import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.View qualified as HieView -import Hir.Parse qualified as Hir -import Hir.Types qualified as Hir import StaticLS.IDE.HiePos import StaticLS.IDE.Hover.Info import StaticLS.IDE.Monad @@ -57,8 +55,11 @@ retrieveHover :: m (Maybe Hover) retrieveHover path lineCol = do pos <- lineColToPos path lineCol - throwIfInThSplice "retriveHover" path pos - runMaybeT $ do + throwIfInThSplice "retrieveHover" path pos + prg <- getHir path + (mVarNode, prgs) <- getResolvedVarAndPrgs prg lineCol + let astResult = varToHover prgs =<< mVarNode + hieResult <- runMaybeT $ do hieFile <- getHieFile path hieView <- getHieView path lineCol' <- lineColToHieLineCol path lineCol @@ -88,6 +89,7 @@ retrieveHover path lineCol = do ) mHieInfo pure $ hoverInfoToHover srcInfo + pure $ astResult <|> hieResult where hoverInfoToHover :: (Maybe LSP.Range, [Text]) -> Hover hoverInfoToHover (mRange, contents) = diff --git a/src/StaticLS/IDE/Hover/Info.hs b/src/StaticLS/IDE/Hover/Info.hs index 3825fd19..502aff48 100644 --- a/src/StaticLS/IDE/Hover/Info.hs +++ b/src/StaticLS/IDE/Hover/Info.hs @@ -1,5 +1,7 @@ module StaticLS.IDE.Hover.Info (hoverInfo) where +import AST.Haskell qualified as H +import Arborist.Renamer import Data.Array import Data.LineColRange (LineColRange) import Data.List.Extra (dropEnd1, nubOrd) diff --git a/src/StaticLS/IDE/InlayHints/Wildcard.hs b/src/StaticLS/IDE/InlayHints/Wildcard.hs index 409d8201..c5466947 100644 --- a/src/StaticLS/IDE/InlayHints/Wildcard.hs +++ b/src/StaticLS/IDE/InlayHints/Wildcard.hs @@ -26,13 +26,13 @@ import Data.Text qualified as Text import GHC.Iface.Ext.Types qualified as GHC import GHC.Plugins as GHC hiding ((<>)) import HieDb (pointCommand) +import Hir.Types qualified as Hir import StaticLS.HI import StaticLS.HI.File import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.View qualified as HieView -import Hir.Types qualified as Hir import StaticLS.IDE.HiePos import StaticLS.IDE.Hover.Info import StaticLS.IDE.InlayHints.Common diff --git a/src/StaticLS/IDE/Monad.hs b/src/StaticLS/IDE/Monad.hs index 520cbf1d..e0bc0b32 100644 --- a/src/StaticLS/IDE/Monad.hs +++ b/src/StaticLS/IDE/Monad.hs @@ -40,6 +40,9 @@ where import AST.Haskell qualified as Haskell import AST.Traversal qualified as AST +import Arborist.Files +import Arborist.ProgramIndex +import Control.Concurrent import Control.Monad import Control.Monad.IO.Class import Control.Monad.Reader @@ -47,6 +50,7 @@ import Control.Monad.Trans.Maybe import Data.ConcurrentCache (ConcurrentCache) import Data.ConcurrentCache qualified as ConcurrentCache import Data.HashMap.Strict qualified as HashMap +import Data.IORef import Data.LineCol (LineCol) import Data.Maybe import Data.Path (AbsPath, toFilePath) @@ -60,11 +64,11 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Time +import Hir qualified as Hir +import Hir.Types qualified as Hir import StaticLS.FilePath import StaticLS.HIE.File qualified as HIE.File import StaticLS.HieView qualified as HieView -import Hir qualified as Hir -import Hir.Types qualified as Hir import StaticLS.Logger import StaticLS.PositionDiff qualified as PositionDiff import StaticLS.Semantic @@ -78,14 +82,18 @@ data IdeEnv = IdeEnv { fileStateCache :: ConcurrentCache AbsPath FileState , hieCache :: ConcurrentCache AbsPath (Maybe CachedHieFile) , diffCache :: ConcurrentCache AbsPath (Maybe DiffCache) + , modFileMap :: ModFileMap + , prgIndex :: MVar ProgramIndex } -newIdeEnv :: IO IdeEnv -newIdeEnv = do +newIdeEnv :: [AbsPath] -> IO IdeEnv +newIdeEnv srcDirs = do fileStateCache <- ConcurrentCache.new hieCache <- ConcurrentCache.new diffCache <- ConcurrentCache.new - pure $ IdeEnv {fileStateCache, hieCache, diffCache} + modFileMap <- buildModuleFileMap (Path.toFilePath <$> srcDirs) + prgIndex <- newMVar HashMap.empty + pure $ IdeEnv {fileStateCache, hieCache, diffCache, modFileMap, prgIndex} class HasIdeEnv m where getIdeEnv :: m IdeEnv @@ -93,6 +101,30 @@ class HasIdeEnv m where instance (HasIdeEnv m, Monad m) => HasIdeEnv (MaybeT m) where getIdeEnv = lift getIdeEnv +getModFileMap :: (MonadIde m) => m ModFileMap +getModFileMap = do + ideEnv <- getIdeEnv + pure ideEnv.modFileMap + +getPrgIndex :: (MonadIde m, MonadIO m) => m ProgramIndex +getPrgIndex = do + ideEnv <- getIdeEnv + liftIO $ readMVar ideEnv.prgIndex + +tryWritePrgIndex :: (MonadIde m, MonadIO m) => (ProgramIndex -> ProgramIndex) -> m () +tryWritePrgIndex modify = do + ideEnv <- getIdeEnv + mCurrPrgIndex <- liftIO $ tryTakeMVar ideEnv.prgIndex + case mCurrPrgIndex of + Nothing -> pure () + Just old -> liftIO $ putMVar ideEnv.prgIndex (modify old) + +invalidatePrgIndexMod :: (MonadIde m, MonadIO m) => Hir.ModuleText -> m () +invalidatePrgIndexMod mod = do + ideEnv <- getIdeEnv + currPrgIndex <- liftIO $ takeMVar ideEnv.prgIndex + liftIO $ putMVar ideEnv.prgIndex (HashMap.delete mod currPrgIndex) + type MonadIde m = ( HasIdeEnv m , MonadIO m @@ -104,6 +136,10 @@ type MonadIde m = removePath :: (MonadIde m) => AbsPath -> m () removePath path = do env <- getIdeEnv + prevHir <- getHir path + case prevHir.mod of + Nothing -> pure () + Just m -> invalidatePrgIndexMod m ConcurrentCache.remove path env.fileStateCache -- setFileState :: (Monad m, HasSemantic m, SetSemantic m) => AbsPath -> FileState -> m () diff --git a/src/StaticLS/IDE/References.hs b/src/StaticLS/IDE/References.hs index 4477661e..6524771d 100644 --- a/src/StaticLS/IDE/References.hs +++ b/src/StaticLS/IDE/References.hs @@ -21,12 +21,12 @@ import Data.Range qualified as Range import Data.Text qualified as T import Database.SQLite.Simple qualified as SQL import HieDb qualified +import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.HIE.File hiding (getHieSource) import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query -import Hir.Types qualified as Hir -import Hir.Parse qualified as Hir import StaticLS.IDE.FileWith (FileLcRange, FileRange, FileWith' (..)) import StaticLS.IDE.HiePos import StaticLS.IDE.Monad diff --git a/src/StaticLS/IDE/Rename.hs b/src/StaticLS/IDE/Rename.hs index 80d6f3a3..194c321a 100644 --- a/src/StaticLS/IDE/Rename.hs +++ b/src/StaticLS/IDE/Rename.hs @@ -22,8 +22,8 @@ import Data.Range qualified as Range import Data.Text (Text) import Data.Text qualified as T import Data.Traversable (for) -import Hir.Types qualified as Hir import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.IDE.FileWith (FileWith' (..)) import StaticLS.IDE.Monad import StaticLS.IDE.References qualified as References diff --git a/src/StaticLS/IDE/Utils.hs b/src/StaticLS/IDE/Utils.hs index 34e8a4d7..970e5b08 100644 --- a/src/StaticLS/IDE/Utils.hs +++ b/src/StaticLS/IDE/Utils.hs @@ -5,8 +5,8 @@ import Control.Monad import Data.Path (AbsPath) import Data.Path qualified as Path import Data.Text qualified as T -import Hir.Types qualified as Hir import Hir.Parse qualified as Hir +import Hir.Types qualified as Hir import StaticLS.Monad import StaticLS.StaticEnv import System.FilePath diff --git a/src/StaticLS/Monad.hs b/src/StaticLS/Monad.hs index 9ae3078c..517765cb 100644 --- a/src/StaticLS/Monad.hs +++ b/src/StaticLS/Monad.hs @@ -1,8 +1,10 @@ module StaticLS.Monad where +import Arborist.Files import Colog.Core.IO qualified as Colog import Control.Monad.Reader import Data.Path (AbsPath) +import Data.Path qualified as Path import StaticLS.IDE.Monad import StaticLS.IDE.Monad qualified as IDE.Monad import StaticLS.Logger @@ -40,7 +42,7 @@ instance HasStaticEnv StaticLsM where initEnv :: AbsPath -> StaticEnvOptions -> Logger -> IO Env initEnv wsRoot staticEnvOptions loggerToUse = do staticEnv <- initStaticEnv wsRoot staticEnvOptions - ideEnv <- IDE.Monad.newIdeEnv + ideEnv <- IDE.Monad.newIdeEnv staticEnv.srcDirs let logger = Colog.liftLogIO loggerToUse pure $ Env diff --git a/static-ls.cabal b/static-ls.cabal index ed140b25..12a91edd 100644 --- a/static-ls.cabal +++ b/static-ls.cabal @@ -75,6 +75,7 @@ library , lsp-types >=2.1.0.0 && <2.4.0.0 , mtl >=2.2.2 && <2.4 , neat-interpolation + , nonempty-containers , optics , parsec >=3.1.0 && <3.2 , pretty-simple @@ -108,6 +109,7 @@ library Data.RangeMap Data.Rope Data.TextUtils + StaticLS.Arborist StaticLS.Except StaticLS.FilePath StaticLS.Handlers @@ -222,6 +224,7 @@ executable print-hie , lsp-types >=2.1.0.0 && <2.4.0.0 , mtl >=2.2.2 && <2.4 , neat-interpolation + , nonempty-containers , optics , optparse-applicative >=0.17.0.0 && <0.19 , parsec >=3.1.0 && <3.2 @@ -292,6 +295,7 @@ executable static-ls , lsp-types >=2.1.0.0 && <2.4.0.0 , mtl >=2.2.2 && <2.4 , neat-interpolation + , nonempty-containers , optics , optparse-applicative >=0.17.0.0 && <0.19 , parsec >=3.1.0 && <3.2 @@ -379,6 +383,7 @@ test-suite expect_tests , lsp-types >=2.1.0.0 && <2.4.0.0 , mtl >=2.2.2 && <2.4 , neat-interpolation + , nonempty-containers , optics , parsec >=3.1.0 && <3.2 , pretty-simple @@ -454,6 +459,7 @@ test-suite static-ls-test , lsp-types >=2.1.0.0 && <2.4.0.0 , mtl >=2.2.2 && <2.4 , neat-interpolation + , nonempty-containers , optics , parsec >=3.1.0 && <3.2 , pretty-simple diff --git a/test/Semantic/HirSpec.hs b/test/Semantic/HirSpec.hs index d1caacb6..bdcaca03 100644 --- a/test/Semantic/HirSpec.hs +++ b/test/Semantic/HirSpec.hs @@ -3,8 +3,8 @@ module Semantic.HirSpec where import AST.Haskell qualified -import NeatInterpolation import Hir.Parse qualified as Hir +import NeatInterpolation import Test.Hspec spec :: Spec diff --git a/test/StaticLS/HirSpec.hs b/test/StaticLS/HirSpec.hs index d1df0c09..1bb66aa1 100644 --- a/test/StaticLS/HirSpec.hs +++ b/test/StaticLS/HirSpec.hs @@ -8,8 +8,8 @@ import Data.Function ((&)) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Read qualified as T.Read -import NeatInterpolation (trimming) import Hir.Parse qualified as Hir +import NeatInterpolation (trimming) import StaticLS.Utils (isJustOrThrowS, isRightOrThrowS) import Test.Hspec import TestImport.Annotation qualified as Annotation From 9fe707792017d80e1e02f4df28430b5a81a03522 Mon Sep 17 00:00:00 2001 From: Joseph Sumabat Date: Thu, 8 May 2025 09:31:50 -0400 Subject: [PATCH 03/21] version update --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index cf55a839..4519d816 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 6b1b2ae2edf5d69a1760d4cb0bee72eea267c1a8 + tag: 340638fd5431fc574e211a2b1960ca47bfdcebdf source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 0b2c5287..d2015fc6 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-4ByyZC4Uu39f/6LnPUQzt8hDZ+ypjz2/726Wlqq6XVQ="; - rev = "6b1b2ae2edf5d69a1760d4cb0bee72eea267c1a8"; + sha256 = "sha256-rUgKJoGust+1Zu7K89AUKr8LHAAr46CUCTWM5cbnU5Y="; + rev = "340638fd5431fc574e211a2b1960ca47bfdcebdf"; fetchSubmodules = true; }; From bcfb2b2af7d744caa23894f9b55e11d25018d842 Mon Sep 17 00:00:00 2001 From: Joseph Sumabat Date: Fri, 16 May 2025 16:26:31 -0400 Subject: [PATCH 04/21] separate mutable and immutable src dirs --- src/StaticLS/Monad.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticLS/Monad.hs b/src/StaticLS/Monad.hs index 517765cb..af1b90bb 100644 --- a/src/StaticLS/Monad.hs +++ b/src/StaticLS/Monad.hs @@ -42,7 +42,7 @@ instance HasStaticEnv StaticLsM where initEnv :: AbsPath -> StaticEnvOptions -> Logger -> IO Env initEnv wsRoot staticEnvOptions loggerToUse = do staticEnv <- initStaticEnv wsRoot staticEnvOptions - ideEnv <- IDE.Monad.newIdeEnv staticEnv.srcDirs + ideEnv <- IDE.Monad.newIdeEnv staticEnv.allSrcDirs let logger = Colog.liftLogIO loggerToUse pure $ Env From 21d6cd7637b9598c6661b06d0cab7db70abd53d0 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Wed, 4 Jun 2025 16:56:15 -0500 Subject: [PATCH 05/21] feat: added auto-export code action --- cabal.project | 2 +- nix/overlays/default.nix | 4 +- src/Data/Change.hs | 28 ------- src/Data/Edit.hs | 50 ------------ src/StaticLS/Arborist.hs | 2 +- src/StaticLS/IDE/CodeActions.hs | 4 +- src/StaticLS/IDE/CodeActions/AutoExport.hs | 93 ++++++++++++++++++---- static-ls.cabal | 2 - 8 files changed, 83 insertions(+), 102 deletions(-) delete mode 100644 src/Data/Change.hs delete mode 100644 src/Data/Edit.hs diff --git a/cabal.project b/cabal.project index 4519d816..ad6bd07f 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 340638fd5431fc574e211a2b1960ca47bfdcebdf + tag: 25bca7ba47c86800dab7ce6d2bb700b4fcbbbb91 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index d2015fc6..81063abc 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-rUgKJoGust+1Zu7K89AUKr8LHAAr46CUCTWM5cbnU5Y="; - rev = "340638fd5431fc574e211a2b1960ca47bfdcebdf"; + sha256 = "sha256-GVcGBL0hj+/7EgoCsUGYAirS12lSQUBljBtAI/nZ/c4="; + rev = "513133e9dde76443c0e32f01927cf725bccffa1e"; fetchSubmodules = true; }; diff --git a/src/Data/Change.hs b/src/Data/Change.hs deleted file mode 100644 index 5b67ac53..00000000 --- a/src/Data/Change.hs +++ /dev/null @@ -1,28 +0,0 @@ -module Data.Change ( - Change (..), - insert, - delete, - replace, -) -where - -import Data.Pos (Pos) -import Data.Range (Range) -import Data.Range qualified as Range -import Data.Text (Text) -import Data.Text qualified as T - -data Change = Change - { insert :: !Text - , delete :: !Range - } - deriving (Show, Eq, Ord) - -insert :: Pos -> Text -> Change -insert pos text = Change {insert = text, delete = Range.empty pos} - -delete :: Range -> Change -delete r = Change {insert = T.empty, delete = r} - -replace :: Range -> Text -> Change -replace r text = Change {insert = text, delete = r} diff --git a/src/Data/Edit.hs b/src/Data/Edit.hs deleted file mode 100644 index 3687a078..00000000 --- a/src/Data/Edit.hs +++ /dev/null @@ -1,50 +0,0 @@ -module Data.Edit ( - Edit, - insert, - delete, - replace, - changesToEdit, - getChanges, - empty, - singleton, -) -where - -import Data.Change (Change) -import Data.Change qualified as Change -import Data.List qualified as List -import Data.Pos (Pos) -import Data.Range (Range (..)) -import Data.Text (Text) - --- Invariant: disjoint, sorted by delete -data Edit = Edit [Change] - deriving (Show, Eq, Ord) - -instance Semigroup Edit where - Edit cs <> Edit cs' = Edit (cs <> cs') - -instance Monoid Edit where - mempty = empty - -insert :: Pos -> Text -> Edit -insert p t = Edit [Change.insert p t] - -delete :: Range -> Edit -delete r = Edit [Change.delete r] - -replace :: Range -> Text -> Edit -replace r t = Edit [Change.replace r t] - --- TODO: change if they are disjoint -changesToEdit :: [Change] -> Edit -changesToEdit = Edit - -getChanges :: Edit -> [Change] -getChanges (Edit cs) = List.sortOn (\c -> (c.delete.start, c.delete.end)) cs - -singleton :: Change -> Edit -singleton = Edit . pure - -empty :: Edit -empty = Edit [] diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 4b01ab43..1560918f 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -3,7 +3,6 @@ module StaticLS.Arborist where import AST qualified import AST.Haskell qualified as H import Arborist.Files -import Arborist.ModGraph import Arborist.Renamer import Arborist.Scope.Types import Control.Error @@ -28,6 +27,7 @@ import StaticLS.IDE.Monad import StaticLS.ProtoLSP qualified as ProtoLSP import System.Directory (doesFileExist) import Arborist.Haddock +import Arborist.ProgramIndex time :: (MonadIO m) => [Char] -> m a -> m a time label fn = do diff --git a/src/StaticLS/IDE/CodeActions.hs b/src/StaticLS/IDE/CodeActions.hs index 52ac6d08..53f303bd 100644 --- a/src/StaticLS/IDE/CodeActions.hs +++ b/src/StaticLS/IDE/CodeActions.hs @@ -11,6 +11,7 @@ import Data.Rope qualified as Rope import StaticLS.IDE.CodeActions.AddTypeSig qualified as AddTypeSig import StaticLS.IDE.CodeActions.AutoImport qualified as AutoImport import StaticLS.IDE.CodeActions.RemoveRedundantImports as RemoveRedundantImports +import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.IDE.SourceEdit (SourceEdit) @@ -25,7 +26,8 @@ getCodeActions path lineCol = do typesCodeActions <- AddTypeSig.codeAction cx importCodeActions <- AutoImport.codeAction cx removeRedundantImports <- RemoveRedundantImports.codeAction cx - let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports + exportCodeActions <- AutoExport.codeAction cx + let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions pure codeActions resolveLazyAssist :: CodeActionMessage -> StaticLsM SourceEdit diff --git a/src/StaticLS/IDE/CodeActions/AutoExport.hs b/src/StaticLS/IDE/CodeActions/AutoExport.hs index 38fae8fa..c11c2ed7 100644 --- a/src/StaticLS/IDE/CodeActions/AutoExport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoExport.hs @@ -8,26 +8,85 @@ import Hir.Types qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.Monad +import AST.Haskell as Haskell +import StaticLS.IDE.SourceEdit as SourceEdit +import Hir.Parse as AST +import Arborist.AutoExport (getAllDeclExportEdit, getDeclExportEdit) +import Data.Path +import Data.Text (Text) --- declToExportItem :: Hir.Decl -> ExportItem --- declToExportItem decl = case decl of --- DeclData d -> ExportItem (getDeclName decl) (AST.getDynNode d.node) --- DeclNewtype n -> ExportItem (getDeclName decl) (AST.getDynNode n.node) --- DeclClass c -> ExportItem (getDeclName decl) (AST.getDynNode c.node) --- DeclSig s -> ExportItem (getDeclName decl) (AST.getDynNode s.node) --- DeclBind b -> ExportItem (getDeclName decl) (AST.getDynNode b.node) --- DeclDataFamily d -> ExportItem (getDeclName decl) (AST.getDynNode d.node) --- DeclPatternSig p -> ExportItem (getDeclName decl) (AST.getDynNode p.node) --- DeclPattern p -> ExportItem (getDeclName decl) (AST.getDynNode p.node) --- DeclTypeFamily t -> ExportItem (getDeclName decl) (AST.getDynNode t.node) --- DeclTypeSynonym t -> ExportItem (getDeclName decl) (AST.getDynNode t.node) +dropModule :: Hir.Qualified -> Hir.Name +dropModule (Hir.Qualified _ name) = name + +qualifiedToText :: Hir.Name -> Text +qualifiedToText nm = AST.nodeToText (nm.node) + +isAlreadyExported :: Hir.Program -> Hir.Decl -> Bool +isAlreadyExported prog decl = + let current = getCurrentExportNames prog + nameTxt = declNameText decl + in nameTxt `elem` current + +getCurrentExportNames :: Hir.Program -> [Text] +getCurrentExportNames prog = + case prog.exports of + Nothing -> [] + Just xs -> map (qualifiedToText . dropModule) (exportItemNames xs) + +isSupportedDecl :: Hir.Decl -> Bool +isSupportedDecl decl = + case decl of + Hir.DeclBind _ -> True + Hir.DeclData _ -> True + Hir.DeclNewtype _ -> True + Hir.DeclClass _ -> True + _ -> False getDeclarationsAtPoint :: Range -> [Hir.Decl] -> [Hir.Decl] getDeclarationsAtPoint range decls = - filter (\decl -> (declDynNode decl).nodeRange `Range.containsRange` range) decls + filter (\decl -> (declName decl).node.nodeRange `Range.containsRange` range) decls + +getHeaderAtPoint :: Range -> Haskell.HeaderP -> Maybe Haskell.HeaderP +getHeaderAtPoint cursorLocation headerP = if (AST.getDynNode headerP).nodeRange `Range.containsRange` cursorLocation + then Just headerP + else Nothing + +mkAssistForAllDecl:: AbsPath -> Hir.Program -> Haskell.HeaderP -> Assist +mkAssistForAllDecl path prog headerP = + let allExportEdit = getAllDeclExportEdit prog headerP + sourceEdit = SourceEdit.single path allExportEdit + label = "Add exports for all declarations" + in + mkAssist label sourceEdit + +mkAssistForDecl :: AbsPath -> Haskell.HeaderP -> Hir.Decl -> Assist +mkAssistForDecl path headerP decl = + let declExportEdit = getDeclExportEdit headerP decl + sourceEdit = SourceEdit.single path declExportEdit + label = "Add export for " <> declNameText decl + in + mkAssist label sourceEdit codeAction :: CodeActionContext -> StaticLsM [Assist] -codeAction cx = do - hir <- getHir cx.path - let _decls = getDeclarationsAtPoint (Range.point cx.pos) hir.decls - pure [] +codeAction CodeActionContext {path, pos} = do + hir <- getHir path + + -- get the decl at the current cursor/highlight pos + let cursorLocation = Range.point pos + allDeclsAtPoint = getDeclarationsAtPoint cursorLocation (filter isSupportedDecl hir.decls) + declsAtPoint = filter (not . isAlreadyExported hir) allDeclsAtPoint + + -- get header + let dynNode = AST.getDynNode hir.node + mHeaderP = AST.findNode (AST.cast @Haskell.HeaderP) dynNode + + case mHeaderP of + Nothing -> pure [] + Just headerP -> do + let mHeaderPCursor = getHeaderAtPoint cursorLocation headerP + assistAll = case mHeaderPCursor of + Just headerP -> [mkAssistForAllDecl path hir headerP] + _ -> [] + assistsPerDecl = map (mkAssistForDecl path headerP) declsAtPoint + + pure (assistAll ++ assistsPerDecl) \ No newline at end of file diff --git a/static-ls.cabal b/static-ls.cabal index 12a91edd..45fa4a84 100644 --- a/static-ls.cabal +++ b/static-ls.cabal @@ -101,10 +101,8 @@ library if flag(dev) ghc-options: -fwrite-ide-info -hiedir .hiefiles -fdefer-type-errors -fno-defer-typed-holes -Werror=deferred-type-errors -Werror=deferred-out-of-scope-variables exposed-modules: - Data.Change Data.ConcurrentCache Data.Diff - Data.Edit Data.ListUtils Data.RangeMap Data.Rope From f42450a551ccb95071f3f0aa7fac5c2f6e857781 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Wed, 4 Jun 2025 16:58:28 -0500 Subject: [PATCH 06/21] fix: hlint + formulu --- src/StaticLS/Arborist.hs | 37 ++++++++++--------- src/StaticLS/IDE/CodeActions.hs | 2 +- src/StaticLS/IDE/CodeActions/AutoExport.hs | 41 +++++++++++----------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 1560918f..c626fdd5 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -3,6 +3,8 @@ module StaticLS.Arborist where import AST qualified import AST.Haskell qualified as H import Arborist.Files +import Arborist.Haddock +import Arborist.ProgramIndex import Arborist.Renamer import Arborist.Scope.Types import Control.Error @@ -26,8 +28,6 @@ import StaticLS.IDE.FileWith import StaticLS.IDE.Monad import StaticLS.ProtoLSP qualified as ProtoLSP import System.Directory (doesFileExist) -import Arborist.Haddock -import Arborist.ProgramIndex time :: (MonadIO m) => [Char] -> m a -> m a time label fn = do @@ -51,14 +51,12 @@ getResolvedVarAndPrgs target lc = do let resolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree pure (resolvedVar, requiredPrograms) - getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo getRequiredHaddock prgIndex varInfo = let mPrg = Map.lookup varInfo.originatingMod prgIndex haddockIndex = maybe Map.empty (indexPrgHaddocks Map.empty) mPrg qualName = glblVarInfoToQualified varInfo - in - Map.lookup qualName haddockIndex + in Map.lookup qualName haddockIndex ------------------- -- Definition @@ -96,32 +94,37 @@ varToHover prgIndex varNode = let mResolvedVar = varNode.ext range = ProtoLSP.lineColRangeToProto varNode.dynNode.nodeLineColRange mContents = (resolvedVarToContents prgIndex =<< mResolvedVar) - in (\contents -> Hover - { _range = Just range - , _contents = InL $ MarkupContent MarkupKind_Markdown contents - }) <$> mContents + in ( \contents -> + Hover + { _range = Just range + , _contents = InL $ MarkupContent MarkupKind_Markdown contents + } + ) + <$> mContents resolvedVarToContents :: ProgramIndex -> ResolvedVariable -> Maybe Text resolvedVarToContents prgIndex resolvedVar = case resolvedVar of ResolvedVariable (ResolvedGlobal glblVarInfo) -> - let mHover = getRequiredHaddock prgIndex glblVarInfo in - Just $ renderGlblVarInfo mHover glblVarInfo + let mHover = getRequiredHaddock prgIndex glblVarInfo + in Just $ renderGlblVarInfo mHover glblVarInfo _ -> Nothing renderGlblVarInfo :: Maybe HaddockInfo -> GlblVarInfo -> Text renderGlblVarInfo mHaddock glblVarInfo = - wrapHaskell - (T.intercalate "\n" - [haddock - ,tySig - ] + wrapHaskell + ( T.intercalate + "\n" + [ haddock + , tySig + ] ) <> " \n\nimported from: *" <> T.intercalate ", " (NE.toList $ (.mod.text) <$> NESet.toList glblVarInfo.importedFrom) <> "*" <> " \noriginates from: *" - <> glblVarInfo.originatingMod.text <> "*" + <> glblVarInfo.originatingMod.text + <> "*" where haddock = maybe "" (.text) mHaddock diff --git a/src/StaticLS/IDE/CodeActions.hs b/src/StaticLS/IDE/CodeActions.hs index 53f303bd..2ff4115d 100644 --- a/src/StaticLS/IDE/CodeActions.hs +++ b/src/StaticLS/IDE/CodeActions.hs @@ -9,9 +9,9 @@ import Data.LineCol (LineCol (..)) import Data.Path (AbsPath) import Data.Rope qualified as Rope import StaticLS.IDE.CodeActions.AddTypeSig qualified as AddTypeSig +import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.AutoImport qualified as AutoImport import StaticLS.IDE.CodeActions.RemoveRedundantImports as RemoveRedundantImports -import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad import StaticLS.IDE.SourceEdit (SourceEdit) diff --git a/src/StaticLS/IDE/CodeActions/AutoExport.hs b/src/StaticLS/IDE/CodeActions/AutoExport.hs index c11c2ed7..f7570fb6 100644 --- a/src/StaticLS/IDE/CodeActions/AutoExport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoExport.hs @@ -1,19 +1,19 @@ module StaticLS.IDE.CodeActions.AutoExport where import AST qualified +import AST.Haskell as Haskell +import Arborist.AutoExport (getAllDeclExportEdit, getDeclExportEdit) +import Data.Path import Data.Range (Range) import Data.Range qualified as Range +import Data.Text (Text) import Hir +import Hir.Parse as AST import Hir.Types qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad -import StaticLS.Monad -import AST.Haskell as Haskell import StaticLS.IDE.SourceEdit as SourceEdit -import Hir.Parse as AST -import Arborist.AutoExport (getAllDeclExportEdit, getDeclExportEdit) -import Data.Path -import Data.Text (Text) +import StaticLS.Monad dropModule :: Hir.Qualified -> Hir.Name dropModule (Hir.Qualified _ name) = name @@ -25,7 +25,7 @@ isAlreadyExported :: Hir.Program -> Hir.Decl -> Bool isAlreadyExported prog decl = let current = getCurrentExportNames prog nameTxt = declNameText decl - in nameTxt `elem` current + in nameTxt `elem` current getCurrentExportNames :: Hir.Program -> [Text] getCurrentExportNames prog = @@ -34,7 +34,7 @@ getCurrentExportNames prog = Just xs -> map (qualifiedToText . dropModule) (exportItemNames xs) isSupportedDecl :: Hir.Decl -> Bool -isSupportedDecl decl = +isSupportedDecl decl = case decl of Hir.DeclBind _ -> True Hir.DeclData _ -> True @@ -47,25 +47,24 @@ getDeclarationsAtPoint range decls = filter (\decl -> (declName decl).node.nodeRange `Range.containsRange` range) decls getHeaderAtPoint :: Range -> Haskell.HeaderP -> Maybe Haskell.HeaderP -getHeaderAtPoint cursorLocation headerP = if (AST.getDynNode headerP).nodeRange `Range.containsRange` cursorLocation - then Just headerP - else Nothing +getHeaderAtPoint cursorLocation headerP = + if (AST.getDynNode headerP).nodeRange `Range.containsRange` cursorLocation + then Just headerP + else Nothing -mkAssistForAllDecl:: AbsPath -> Hir.Program -> Haskell.HeaderP -> Assist +mkAssistForAllDecl :: AbsPath -> Hir.Program -> Haskell.HeaderP -> Assist mkAssistForAllDecl path prog headerP = let allExportEdit = getAllDeclExportEdit prog headerP sourceEdit = SourceEdit.single path allExportEdit - label = "Add exports for all declarations" - in - mkAssist label sourceEdit + label = "Add exports for all declarations" + in mkAssist label sourceEdit mkAssistForDecl :: AbsPath -> Haskell.HeaderP -> Hir.Decl -> Assist mkAssistForDecl path headerP decl = let declExportEdit = getDeclExportEdit headerP decl sourceEdit = SourceEdit.single path declExportEdit label = "Add export for " <> declNameText decl - in - mkAssist label sourceEdit + in mkAssist label sourceEdit codeAction :: CodeActionContext -> StaticLsM [Assist] codeAction CodeActionContext {path, pos} = do @@ -77,7 +76,7 @@ codeAction CodeActionContext {path, pos} = do declsAtPoint = filter (not . isAlreadyExported hir) allDeclsAtPoint -- get header - let dynNode = AST.getDynNode hir.node + let dynNode = AST.getDynNode hir.node mHeaderP = AST.findNode (AST.cast @Haskell.HeaderP) dynNode case mHeaderP of @@ -85,8 +84,8 @@ codeAction CodeActionContext {path, pos} = do Just headerP -> do let mHeaderPCursor = getHeaderAtPoint cursorLocation headerP assistAll = case mHeaderPCursor of - Just headerP -> [mkAssistForAllDecl path hir headerP] - _ -> [] + Just headerP -> [mkAssistForAllDecl path hir headerP] + _ -> [] assistsPerDecl = map (mkAssistForDecl path headerP) declsAtPoint - pure (assistAll ++ assistsPerDecl) \ No newline at end of file + pure (assistAll ++ assistsPerDecl) From 591fc2cb14d93e5f263e8b4cf96bfaee27a75928 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Wed, 4 Jun 2025 17:05:00 -0500 Subject: [PATCH 07/21] fix: update nix and cabal link to haskell-arborist --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index ad6bd07f..c29b881d 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 25bca7ba47c86800dab7ce6d2bb700b4fcbbbb91 + tag: 2d9813f278f5a9547186b5a326cf99b4e7d99dab source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 81063abc..6a1ec84f 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-GVcGBL0hj+/7EgoCsUGYAirS12lSQUBljBtAI/nZ/c4="; - rev = "513133e9dde76443c0e32f01927cf725bccffa1e"; + sha256 = "sha256-CroTvgRJN3vebLexf2saFYVQrg4cBEqLKUegZ0ZfYIE="; + rev = "2d9813f278f5a9547186b5a326cf99b4e7d99dab"; fetchSubmodules = true; }; From 526cb785d54cbd8ed85339449cd8d52d0e0ba5ad Mon Sep 17 00:00:00 2001 From: ShanAli0640 <66219298+ShanAli0640@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:47:44 -0500 Subject: [PATCH 08/21] Shan/haskell-arborist/auto-export (#160) This PR adds the "Auto Export" code action to static-ls. Users can now automatically add their declarations to the export list in a file by clicking on the code action which prompts "Add export for {declaration name}". They can also add all the declarations in a file to the export list in one go by clicking "Add exports for all declarations" when hovering over the module header. --------- Co-authored-by: Shan Ali --- cabal.project | 2 +- nix/overlays/default.nix | 4 +- src/Data/Change.hs | 28 ------- src/Data/Edit.hs | 50 ------------ src/StaticLS/Arborist.hs | 37 +++++---- src/StaticLS/IDE/CodeActions.hs | 4 +- src/StaticLS/IDE/CodeActions/AutoExport.hs | 92 ++++++++++++++++++---- src/StaticLS/IDE/Definition.hs | 2 +- static-ls.cabal | 2 - 9 files changed, 102 insertions(+), 119 deletions(-) delete mode 100644 src/Data/Change.hs delete mode 100644 src/Data/Edit.hs diff --git a/cabal.project b/cabal.project index 4519d816..c29b881d 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 340638fd5431fc574e211a2b1960ca47bfdcebdf + tag: 2d9813f278f5a9547186b5a326cf99b4e7d99dab source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index d2015fc6..6a1ec84f 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-rUgKJoGust+1Zu7K89AUKr8LHAAr46CUCTWM5cbnU5Y="; - rev = "340638fd5431fc574e211a2b1960ca47bfdcebdf"; + sha256 = "sha256-CroTvgRJN3vebLexf2saFYVQrg4cBEqLKUegZ0ZfYIE="; + rev = "2d9813f278f5a9547186b5a326cf99b4e7d99dab"; fetchSubmodules = true; }; diff --git a/src/Data/Change.hs b/src/Data/Change.hs deleted file mode 100644 index 5b67ac53..00000000 --- a/src/Data/Change.hs +++ /dev/null @@ -1,28 +0,0 @@ -module Data.Change ( - Change (..), - insert, - delete, - replace, -) -where - -import Data.Pos (Pos) -import Data.Range (Range) -import Data.Range qualified as Range -import Data.Text (Text) -import Data.Text qualified as T - -data Change = Change - { insert :: !Text - , delete :: !Range - } - deriving (Show, Eq, Ord) - -insert :: Pos -> Text -> Change -insert pos text = Change {insert = text, delete = Range.empty pos} - -delete :: Range -> Change -delete r = Change {insert = T.empty, delete = r} - -replace :: Range -> Text -> Change -replace r text = Change {insert = text, delete = r} diff --git a/src/Data/Edit.hs b/src/Data/Edit.hs deleted file mode 100644 index 3687a078..00000000 --- a/src/Data/Edit.hs +++ /dev/null @@ -1,50 +0,0 @@ -module Data.Edit ( - Edit, - insert, - delete, - replace, - changesToEdit, - getChanges, - empty, - singleton, -) -where - -import Data.Change (Change) -import Data.Change qualified as Change -import Data.List qualified as List -import Data.Pos (Pos) -import Data.Range (Range (..)) -import Data.Text (Text) - --- Invariant: disjoint, sorted by delete -data Edit = Edit [Change] - deriving (Show, Eq, Ord) - -instance Semigroup Edit where - Edit cs <> Edit cs' = Edit (cs <> cs') - -instance Monoid Edit where - mempty = empty - -insert :: Pos -> Text -> Edit -insert p t = Edit [Change.insert p t] - -delete :: Range -> Edit -delete r = Edit [Change.delete r] - -replace :: Range -> Text -> Edit -replace r t = Edit [Change.replace r t] - --- TODO: change if they are disjoint -changesToEdit :: [Change] -> Edit -changesToEdit = Edit - -getChanges :: Edit -> [Change] -getChanges (Edit cs) = List.sortOn (\c -> (c.delete.start, c.delete.end)) cs - -singleton :: Change -> Edit -singleton = Edit . pure - -empty :: Edit -empty = Edit [] diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 4b01ab43..c626fdd5 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -3,7 +3,8 @@ module StaticLS.Arborist where import AST qualified import AST.Haskell qualified as H import Arborist.Files -import Arborist.ModGraph +import Arborist.Haddock +import Arborist.ProgramIndex import Arborist.Renamer import Arborist.Scope.Types import Control.Error @@ -27,7 +28,6 @@ import StaticLS.IDE.FileWith import StaticLS.IDE.Monad import StaticLS.ProtoLSP qualified as ProtoLSP import System.Directory (doesFileExist) -import Arborist.Haddock time :: (MonadIO m) => [Char] -> m a -> m a time label fn = do @@ -51,14 +51,12 @@ getResolvedVarAndPrgs target lc = do let resolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree pure (resolvedVar, requiredPrograms) - getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo getRequiredHaddock prgIndex varInfo = let mPrg = Map.lookup varInfo.originatingMod prgIndex haddockIndex = maybe Map.empty (indexPrgHaddocks Map.empty) mPrg qualName = glblVarInfoToQualified varInfo - in - Map.lookup qualName haddockIndex + in Map.lookup qualName haddockIndex ------------------- -- Definition @@ -96,32 +94,37 @@ varToHover prgIndex varNode = let mResolvedVar = varNode.ext range = ProtoLSP.lineColRangeToProto varNode.dynNode.nodeLineColRange mContents = (resolvedVarToContents prgIndex =<< mResolvedVar) - in (\contents -> Hover - { _range = Just range - , _contents = InL $ MarkupContent MarkupKind_Markdown contents - }) <$> mContents + in ( \contents -> + Hover + { _range = Just range + , _contents = InL $ MarkupContent MarkupKind_Markdown contents + } + ) + <$> mContents resolvedVarToContents :: ProgramIndex -> ResolvedVariable -> Maybe Text resolvedVarToContents prgIndex resolvedVar = case resolvedVar of ResolvedVariable (ResolvedGlobal glblVarInfo) -> - let mHover = getRequiredHaddock prgIndex glblVarInfo in - Just $ renderGlblVarInfo mHover glblVarInfo + let mHover = getRequiredHaddock prgIndex glblVarInfo + in Just $ renderGlblVarInfo mHover glblVarInfo _ -> Nothing renderGlblVarInfo :: Maybe HaddockInfo -> GlblVarInfo -> Text renderGlblVarInfo mHaddock glblVarInfo = - wrapHaskell - (T.intercalate "\n" - [haddock - ,tySig - ] + wrapHaskell + ( T.intercalate + "\n" + [ haddock + , tySig + ] ) <> " \n\nimported from: *" <> T.intercalate ", " (NE.toList $ (.mod.text) <$> NESet.toList glblVarInfo.importedFrom) <> "*" <> " \noriginates from: *" - <> glblVarInfo.originatingMod.text <> "*" + <> glblVarInfo.originatingMod.text + <> "*" where haddock = maybe "" (.text) mHaddock diff --git a/src/StaticLS/IDE/CodeActions.hs b/src/StaticLS/IDE/CodeActions.hs index 52ac6d08..2ff4115d 100644 --- a/src/StaticLS/IDE/CodeActions.hs +++ b/src/StaticLS/IDE/CodeActions.hs @@ -9,6 +9,7 @@ import Data.LineCol (LineCol (..)) import Data.Path (AbsPath) import Data.Rope qualified as Rope import StaticLS.IDE.CodeActions.AddTypeSig qualified as AddTypeSig +import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.AutoImport qualified as AutoImport import StaticLS.IDE.CodeActions.RemoveRedundantImports as RemoveRedundantImports import StaticLS.IDE.CodeActions.Types @@ -25,7 +26,8 @@ getCodeActions path lineCol = do typesCodeActions <- AddTypeSig.codeAction cx importCodeActions <- AutoImport.codeAction cx removeRedundantImports <- RemoveRedundantImports.codeAction cx - let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports + exportCodeActions <- AutoExport.codeAction cx + let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions pure codeActions resolveLazyAssist :: CodeActionMessage -> StaticLsM SourceEdit diff --git a/src/StaticLS/IDE/CodeActions/AutoExport.hs b/src/StaticLS/IDE/CodeActions/AutoExport.hs index 38fae8fa..f7570fb6 100644 --- a/src/StaticLS/IDE/CodeActions/AutoExport.hs +++ b/src/StaticLS/IDE/CodeActions/AutoExport.hs @@ -1,33 +1,91 @@ module StaticLS.IDE.CodeActions.AutoExport where import AST qualified +import AST.Haskell as Haskell +import Arborist.AutoExport (getAllDeclExportEdit, getDeclExportEdit) +import Data.Path import Data.Range (Range) import Data.Range qualified as Range +import Data.Text (Text) import Hir +import Hir.Parse as AST import Hir.Types qualified as Hir import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad +import StaticLS.IDE.SourceEdit as SourceEdit import StaticLS.Monad --- declToExportItem :: Hir.Decl -> ExportItem --- declToExportItem decl = case decl of --- DeclData d -> ExportItem (getDeclName decl) (AST.getDynNode d.node) --- DeclNewtype n -> ExportItem (getDeclName decl) (AST.getDynNode n.node) --- DeclClass c -> ExportItem (getDeclName decl) (AST.getDynNode c.node) --- DeclSig s -> ExportItem (getDeclName decl) (AST.getDynNode s.node) --- DeclBind b -> ExportItem (getDeclName decl) (AST.getDynNode b.node) --- DeclDataFamily d -> ExportItem (getDeclName decl) (AST.getDynNode d.node) --- DeclPatternSig p -> ExportItem (getDeclName decl) (AST.getDynNode p.node) --- DeclPattern p -> ExportItem (getDeclName decl) (AST.getDynNode p.node) --- DeclTypeFamily t -> ExportItem (getDeclName decl) (AST.getDynNode t.node) --- DeclTypeSynonym t -> ExportItem (getDeclName decl) (AST.getDynNode t.node) +dropModule :: Hir.Qualified -> Hir.Name +dropModule (Hir.Qualified _ name) = name + +qualifiedToText :: Hir.Name -> Text +qualifiedToText nm = AST.nodeToText (nm.node) + +isAlreadyExported :: Hir.Program -> Hir.Decl -> Bool +isAlreadyExported prog decl = + let current = getCurrentExportNames prog + nameTxt = declNameText decl + in nameTxt `elem` current + +getCurrentExportNames :: Hir.Program -> [Text] +getCurrentExportNames prog = + case prog.exports of + Nothing -> [] + Just xs -> map (qualifiedToText . dropModule) (exportItemNames xs) + +isSupportedDecl :: Hir.Decl -> Bool +isSupportedDecl decl = + case decl of + Hir.DeclBind _ -> True + Hir.DeclData _ -> True + Hir.DeclNewtype _ -> True + Hir.DeclClass _ -> True + _ -> False getDeclarationsAtPoint :: Range -> [Hir.Decl] -> [Hir.Decl] getDeclarationsAtPoint range decls = - filter (\decl -> (declDynNode decl).nodeRange `Range.containsRange` range) decls + filter (\decl -> (declName decl).node.nodeRange `Range.containsRange` range) decls + +getHeaderAtPoint :: Range -> Haskell.HeaderP -> Maybe Haskell.HeaderP +getHeaderAtPoint cursorLocation headerP = + if (AST.getDynNode headerP).nodeRange `Range.containsRange` cursorLocation + then Just headerP + else Nothing + +mkAssistForAllDecl :: AbsPath -> Hir.Program -> Haskell.HeaderP -> Assist +mkAssistForAllDecl path prog headerP = + let allExportEdit = getAllDeclExportEdit prog headerP + sourceEdit = SourceEdit.single path allExportEdit + label = "Add exports for all declarations" + in mkAssist label sourceEdit + +mkAssistForDecl :: AbsPath -> Haskell.HeaderP -> Hir.Decl -> Assist +mkAssistForDecl path headerP decl = + let declExportEdit = getDeclExportEdit headerP decl + sourceEdit = SourceEdit.single path declExportEdit + label = "Add export for " <> declNameText decl + in mkAssist label sourceEdit codeAction :: CodeActionContext -> StaticLsM [Assist] -codeAction cx = do - hir <- getHir cx.path - let _decls = getDeclarationsAtPoint (Range.point cx.pos) hir.decls - pure [] +codeAction CodeActionContext {path, pos} = do + hir <- getHir path + + -- get the decl at the current cursor/highlight pos + let cursorLocation = Range.point pos + allDeclsAtPoint = getDeclarationsAtPoint cursorLocation (filter isSupportedDecl hir.decls) + declsAtPoint = filter (not . isAlreadyExported hir) allDeclsAtPoint + + -- get header + let dynNode = AST.getDynNode hir.node + mHeaderP = AST.findNode (AST.cast @Haskell.HeaderP) dynNode + + case mHeaderP of + Nothing -> pure [] + Just headerP -> do + let mHeaderPCursor = getHeaderAtPoint cursorLocation headerP + assistAll = case mHeaderPCursor of + Just headerP -> [mkAssistForAllDecl path hir headerP] + _ -> [] + assistsPerDecl = map (mkAssistForDecl path headerP) declsAtPoint + + pure (assistAll ++ assistsPerDecl) diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 4fd358ce..dcb72b18 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -209,4 +209,4 @@ convertPersistentModelFileLc fileLc = do Nothing -> pure [fileLc] Just persistentModelName -> do res <- persistentModelNameToFileLc persistentModelName - pure $ maybeToList res <> [fileLc] + pure $ maybeToList res <> [fileLc] \ No newline at end of file diff --git a/static-ls.cabal b/static-ls.cabal index 12a91edd..45fa4a84 100644 --- a/static-ls.cabal +++ b/static-ls.cabal @@ -101,10 +101,8 @@ library if flag(dev) ghc-options: -fwrite-ide-info -hiedir .hiefiles -fdefer-type-errors -fno-defer-typed-holes -Werror=deferred-type-errors -Werror=deferred-out-of-scope-variables exposed-modules: - Data.Change Data.ConcurrentCache Data.Diff - Data.Edit Data.ListUtils Data.RangeMap Data.Rope From f412cc9017c32aa99b25551f8d31876d58c1c3cc Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Tue, 10 Jun 2025 13:59:03 -0500 Subject: [PATCH 09/21] feat: go to definition for names --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- src/StaticLS/Arborist.hs | 25 +++++++++++++++++++------ src/StaticLS/IDE/Definition.hs | 23 +++++++++++++++++------ src/StaticLS/IDE/Hover.hs | 2 +- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/cabal.project b/cabal.project index c29b881d..fda6bc13 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 2d9813f278f5a9547186b5a326cf99b4e7d99dab + tag: a20f13c8b06dedfa468e24c7e205386517cdf954 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 6a1ec84f..453f6dbd 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-CroTvgRJN3vebLexf2saFYVQrg4cBEqLKUegZ0ZfYIE="; - rev = "2d9813f278f5a9547186b5a326cf99b4e7d99dab"; + sha256 = "sha256-pn76y2jRe+RqBJmi2ESs14J7z46qnBT2QcSmnXFXu1s="; + rev = "a20f13c8b06dedfa468e24c7e205386517cdf954"; fetchSubmodules = true; }; diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index c626fdd5..a55e697b 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -10,8 +10,8 @@ import Arborist.Scope.Types import Control.Error import Control.Monad qualified as Monad import Control.Monad.IO.Class -import Data.HashMap.Lazy qualified as HashMap import Data.HashMap.Lazy qualified as Map +import Data.HashMap.Lazy qualified as HashMap import Data.LineCol (LineCol (..)) import Data.LineColRange import Data.List qualified as List @@ -37,19 +37,24 @@ time label fn = do traceShowM $ "Time to run " <> label <> ": " ++ show (diffUTCTime end start) pure res + getResolvedVar :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase)) -getResolvedVar target lc = fst <$> getResolvedVarAndPrgs target lc +getResolvedVar target lc = (\(var, _, _) -> var) <$> getResolvedVarAndPrgs target lc + +getResolvedName :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Name RenamePhase)) +getResolvedName target lc = (\(_, name, _) -> name) <$> getResolvedVarAndPrgs target lc -getResolvedVarAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase), ProgramIndex) +getResolvedVarAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase), Maybe (H.Name RenamePhase), ProgramIndex) getResolvedVarAndPrgs target lc = do - time "resolvedVar" $ do + time "resolvedVarAndName" $ do modFileMap <- getModFileMap prgIndex <- getPrgIndex (requiredPrograms) <- liftIO $ time "gather" $ gatherScopeDeps prgIndex target modFileMap (Just 2) tryWritePrgIndex (\_ -> requiredPrograms) let renameTree = renamePrg requiredPrograms HashMap.empty target - let resolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree - pure (resolvedVar, requiredPrograms) + mResolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree + mResolvedName = (AST.getDeepestContainingLineCol @(H.Name RenamePhase) (point lc)) . (.dynNode) =<< renameTree + in pure (mResolvedVar, mResolvedName, requiredPrograms) getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo getRequiredHaddock prgIndex varInfo = @@ -85,6 +90,12 @@ varToFileLcRange modFileMap thisMod varNode = do fileLcRanges <- Monad.join <$> (mapM (modLocToFileLcRange modFileMap) locLst) pure fileLcRanges +nameToFileLcRange :: (MonadIO m) => ModFileMap -> H.Name RenamePhase -> m [FileLcRange] +nameToFileLcRange modFileMap nameNode = do + let locLst = maybe [] resolvedNameLocs nameNode.ext + fileLcRanges <- Monad.join <$> mapM (modLocToFileLcRange modFileMap) locLst + pure fileLcRanges + ------------------- -- Hover ------------------- @@ -131,3 +142,5 @@ renderGlblVarInfo mHaddock glblVarInfo = tySig = maybe "" (.node.dynNode.nodeText) glblVarInfo.sig wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" + + diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 4fd358ce..48284f9f 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -59,7 +59,7 @@ getDefinition path lineCol = do case hieDef of [] -> arboristDef _ -> hieDef - +-- DUX 3409 getArboristDefinition :: (MonadIde m, MonadIO m) => AbsPath -> @@ -69,11 +69,22 @@ getArboristDefinition path lineCol = do modFileMap <- getModFileMap prg <- getHir path mVarNode <- getResolvedVar prg lineCol - let mResult = do - varNode <- mVarNode - modText <- prg.mod - pure $ varToFileLcRange modFileMap modText varNode - fromMaybe (pure []) mResult + case mVarNode of + Just varNode -> do + let mResult = do + modText <- prg.mod + pure $ varToFileLcRange modFileMap modText varNode + in fromMaybe (pure []) mResult + Nothing -> do + mNameNode <- getResolvedName prg lineCol + case mNameNode of + Just nameNode -> + let mResult = pure $ nameToFileLcRange modFileMap nameNode + in fromMaybe (pure []) mResult + Nothing -> + pure [] + + getHieDefinition :: (MonadIde m, MonadIO m) => diff --git a/src/StaticLS/IDE/Hover.hs b/src/StaticLS/IDE/Hover.hs index f122b56a..a62d05cb 100644 --- a/src/StaticLS/IDE/Hover.hs +++ b/src/StaticLS/IDE/Hover.hs @@ -57,7 +57,7 @@ retrieveHover path lineCol = do pos <- lineColToPos path lineCol throwIfInThSplice "retrieveHover" path pos prg <- getHir path - (mVarNode, prgs) <- getResolvedVarAndPrgs prg lineCol + (mVarNode, mNameNode, prgs) <- getResolvedVarAndPrgs prg lineCol let astResult = varToHover prgs =<< mVarNode hieResult <- runMaybeT $ do hieFile <- getHieFile path From c2b99f63688e9262ac0db1bdf10a6a425bccd837 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Wed, 11 Jun 2025 13:48:57 -0500 Subject: [PATCH 10/21] fix: updated resolved to use sum type Resolveable --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- src/StaticLS/Arborist.hs | 40 +++++++++++++++------------------- src/StaticLS/IDE/Definition.hs | 39 +++++++++++++-------------------- src/StaticLS/IDE/Hover.hs | 9 ++++++-- 5 files changed, 42 insertions(+), 52 deletions(-) diff --git a/cabal.project b/cabal.project index fda6bc13..35ac0b90 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: a20f13c8b06dedfa468e24c7e205386517cdf954 + tag: bb5dd066305addbac81cb6f1bf740295595bcaef source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 453f6dbd..3ac638ac 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-pn76y2jRe+RqBJmi2ESs14J7z46qnBT2QcSmnXFXu1s="; - rev = "a20f13c8b06dedfa468e24c7e205386517cdf954"; + sha256 = "sha256-vctDkDltFDIkIasp6+qgpjdnpLp8akYqf+o5WuXgMJo="; + rev = "bb5dd066305addbac81cb6f1bf740295595bcaef"; fetchSubmodules = true; }; diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index a55e697b..5ea37289 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -28,6 +28,9 @@ import StaticLS.IDE.FileWith import StaticLS.IDE.Monad import StaticLS.ProtoLSP qualified as ProtoLSP import System.Directory (doesFileExist) +import AST.Sum (Nil, (:+), pattern Inj) + +type Resolveable = H.Variable RenamePhase :+ H.Name RenamePhase :+ Nil time :: (MonadIO m) => [Char] -> m a -> m a time label fn = do @@ -37,24 +40,19 @@ time label fn = do traceShowM $ "Time to run " <> label <> ": " ++ show (diffUTCTime end start) pure res +getResolved :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (Resolveable)) +getResolved target lc = fst <$> getResolvedTermAndPrgs target lc -getResolvedVar :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase)) -getResolvedVar target lc = (\(var, _, _) -> var) <$> getResolvedVarAndPrgs target lc - -getResolvedName :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Name RenamePhase)) -getResolvedName target lc = (\(_, name, _) -> name) <$> getResolvedVarAndPrgs target lc - -getResolvedVarAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (H.Variable RenamePhase), Maybe (H.Name RenamePhase), ProgramIndex) -getResolvedVarAndPrgs target lc = do - time "resolvedVarAndName" $ do +getResolvedTermAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (Resolveable), ProgramIndex) +getResolvedTermAndPrgs target lc = do + time "resolved" $ do modFileMap <- getModFileMap prgIndex <- getPrgIndex - (requiredPrograms) <- liftIO $ time "gather" $ gatherScopeDeps prgIndex target modFileMap (Just 2) + requiredPrograms <- liftIO $ time "gather" $ gatherScopeDeps prgIndex target modFileMap (Just 2) tryWritePrgIndex (\_ -> requiredPrograms) let renameTree = renamePrg requiredPrograms HashMap.empty target - mResolvedVar = (AST.getDeepestContainingLineCol @(H.Variable RenamePhase) (point lc)) . (.dynNode) =<< renameTree - mResolvedName = (AST.getDeepestContainingLineCol @(H.Name RenamePhase) (point lc)) . (.dynNode) =<< renameTree - in pure (mResolvedVar, mResolvedName, requiredPrograms) + mResolved = (AST.getDeepestContainingLineCol @Resolveable (point lc)) . (.dynNode) =<< renameTree + pure (mResolved, requiredPrograms) getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo getRequiredHaddock prgIndex varInfo = @@ -84,15 +82,12 @@ modLocToFileLcRange modFileMap (modText, lcRange) = do else pure Nothing pure pathList -varToFileLcRange :: (MonadIO m) => ModFileMap -> Hir.ModuleText -> (H.Variable RenamePhase) -> m [FileLcRange] -varToFileLcRange modFileMap thisMod varNode = do - let locLst = maybe [] (resolvedLocs thisMod) varNode.ext - fileLcRanges <- Monad.join <$> (mapM (modLocToFileLcRange modFileMap) locLst) - pure fileLcRanges - -nameToFileLcRange :: (MonadIO m) => ModFileMap -> H.Name RenamePhase -> m [FileLcRange] -nameToFileLcRange modFileMap nameNode = do - let locLst = maybe [] resolvedNameLocs nameNode.ext +resolvedToFileLcRange :: (MonadIO m) => ModFileMap -> Hir.ModuleText -> Resolveable -> m [FileLcRange] +resolvedToFileLcRange modFileMap thisMod resolved = do + let locLst = case resolved of + Inj @(H.Variable RenamePhase) var -> maybe [] (resolvedLocs thisMod) var.ext + Inj @(H.Name RenamePhase) name -> maybe [] resolvedNameLocs name.ext + _ -> [] fileLcRanges <- Monad.join <$> mapM (modLocToFileLcRange modFileMap) locLst pure fileLcRanges @@ -143,4 +138,3 @@ renderGlblVarInfo mHaddock glblVarInfo = maybe "" (.node.dynNode.nodeText) glblVarInfo.sig wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" - diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 48284f9f..759186b9 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -4,14 +4,11 @@ module StaticLS.IDE.Definition ( nameToLocation, ) where -import AST qualified -import Control.Applicative import Control.Error import Control.Monad qualified as Monad import Control.Monad.Extra (mapMaybeM) import Control.Monad.Reader import Data.LineCol (LineCol (..)) -import Data.LineColRange import Data.LineColRange qualified as LineColRange import Data.List (isSuffixOf) import Data.Maybe qualified as Maybe @@ -23,17 +20,13 @@ import Data.Range qualified as Range import Data.Rope qualified as Rope import Data.Text (Text) import Data.Text qualified as T -import Database.SQLite.Simple qualified as SQL import GHC qualified import GHC.Types.Name qualified as GHC -import HieDb (HieDb) -import HieDb qualified import Hir.Parse qualified as Hir import Hir.Types qualified as Hir import StaticLS.Arborist import StaticLS.FilePath import StaticLS.HIE.File -import StaticLS.HIE.Position import StaticLS.HieView.Name qualified as HieView.Name import StaticLS.HieView.Query qualified as HieView.Query import StaticLS.HieView.Type qualified as HieView.Type @@ -46,6 +39,9 @@ import StaticLS.Logger import StaticLS.StaticEnv import System.Directory (doesFileExist) import System.Directory qualified as Directory +import AST.Sum (pattern Inj) +import AST.Haskell qualified as H +import Arborist.Renamer getDefinition :: (MonadIde m, MonadIO m) => @@ -59,7 +55,7 @@ getDefinition path lineCol = do case hieDef of [] -> arboristDef _ -> hieDef --- DUX 3409 + getArboristDefinition :: (MonadIde m, MonadIO m) => AbsPath -> @@ -68,22 +64,17 @@ getArboristDefinition :: getArboristDefinition path lineCol = do modFileMap <- getModFileMap prg <- getHir path - mVarNode <- getResolvedVar prg lineCol - case mVarNode of - Just varNode -> do - let mResult = do - modText <- prg.mod - pure $ varToFileLcRange modFileMap modText varNode - in fromMaybe (pure []) mResult - Nothing -> do - mNameNode <- getResolvedName prg lineCol - case mNameNode of - Just nameNode -> - let mResult = pure $ nameToFileLcRange modFileMap nameNode - in fromMaybe (pure []) mResult - Nothing -> - pure [] - + mResolved <- getResolved prg lineCol + case mResolved of + Just resolved@(Inj @(H.Variable RenamePhase) _) -> + case prg.mod of + Just modText -> resolvedToFileLcRange modFileMap modText resolved + Nothing -> pure [] + Just resolved@(Inj @(H.Name RenamePhase) _) -> + case prg.mod of + Just modText -> resolvedToFileLcRange modFileMap modText resolved + Nothing -> pure [] + _ -> pure [] getHieDefinition :: diff --git a/src/StaticLS/IDE/Hover.hs b/src/StaticLS/IDE/Hover.hs index a62d05cb..b81bc45c 100644 --- a/src/StaticLS/IDE/Hover.hs +++ b/src/StaticLS/IDE/Hover.hs @@ -45,6 +45,8 @@ import StaticLS.IDE.Monad import StaticLS.Logger (logInfo) import StaticLS.Maybe import StaticLS.ProtoLSP qualified as ProtoLSP +import AST.Sum (pattern Inj) +import Arborist.Renamer -- | Retrieve hover information. retrieveHover :: @@ -57,8 +59,11 @@ retrieveHover path lineCol = do pos <- lineColToPos path lineCol throwIfInThSplice "retrieveHover" path pos prg <- getHir path - (mVarNode, mNameNode, prgs) <- getResolvedVarAndPrgs prg lineCol - let astResult = varToHover prgs =<< mVarNode + (mResolved, prgs) <- getResolvedTermAndPrgs prg lineCol + let astResult = case mResolved of + Just (Inj @(H.Variable RenamePhase) var) -> varToHover prgs var + Just (Inj @(H.Name RenamePhase) _) -> Nothing + _ -> Nothing hieResult <- runMaybeT $ do hieFile <- getHieFile path hieView <- getHieView path From e9aac58eb43a648328fd29a07d909d11f4b9ac04 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Fri, 13 Jun 2025 11:36:36 -0500 Subject: [PATCH 11/21] fix: displaying decl on hover --- cabal.project | 2 +- nix/overlays/default.nix | 4 +-- src/StaticLS/Arborist.hs | 76 ++++++++++++++++++++++++++++++++++----- src/StaticLS/IDE/Hover.hs | 2 +- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/cabal.project b/cabal.project index 35ac0b90..15707cee 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: bb5dd066305addbac81cb6f1bf740295595bcaef + tag: 5477e35ab1f44e3d1fc15225bd478bebce02c173 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 3ac638ac..38c41031 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-vctDkDltFDIkIasp6+qgpjdnpLp8akYqf+o5WuXgMJo="; - rev = "bb5dd066305addbac81cb6f1bf740295595bcaef"; + sha256 = "sha256-+k0Xpj1D6sQec0C4JU+rri86i5KYdCI+3p6O4QPuXiQ="; + rev = "5477e35ab1f44e3d1fc15225bd478bebce02c173"; fetchSubmodules = true; }; diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 5ea37289..61c75ec2 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -28,9 +28,10 @@ import StaticLS.IDE.FileWith import StaticLS.IDE.Monad import StaticLS.ProtoLSP qualified as ProtoLSP import System.Directory (doesFileExist) -import AST.Sum (Nil, (:+), pattern Inj) +import AST.Sum (pattern Inj) + +type ResolveableRename = Resolveable RenamePhase -type Resolveable = H.Variable RenamePhase :+ H.Name RenamePhase :+ Nil time :: (MonadIO m) => [Char] -> m a -> m a time label fn = do @@ -40,10 +41,10 @@ time label fn = do traceShowM $ "Time to run " <> label <> ": " ++ show (diffUTCTime end start) pure res -getResolved :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (Resolveable)) +getResolved :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (ResolveableRename)) getResolved target lc = fst <$> getResolvedTermAndPrgs target lc -getResolvedTermAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (Resolveable), ProgramIndex) +getResolvedTermAndPrgs :: (MonadIO m, MonadIde m) => Hir.Program -> LineCol -> m (Maybe (ResolveableRename), ProgramIndex) getResolvedTermAndPrgs target lc = do time "resolved" $ do modFileMap <- getModFileMap @@ -51,16 +52,23 @@ getResolvedTermAndPrgs target lc = do requiredPrograms <- liftIO $ time "gather" $ gatherScopeDeps prgIndex target modFileMap (Just 2) tryWritePrgIndex (\_ -> requiredPrograms) let renameTree = renamePrg requiredPrograms HashMap.empty target - mResolved = (AST.getDeepestContainingLineCol @Resolveable (point lc)) . (.dynNode) =<< renameTree + mResolved = (AST.getDeepestContainingLineCol @ResolveableRename (point lc)) . (.dynNode) =<< renameTree pure (mResolved, requiredPrograms) -getRequiredHaddock :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo -getRequiredHaddock prgIndex varInfo = +getRequiredHaddockVar :: ProgramIndex -> GlblVarInfo -> Maybe HaddockInfo +getRequiredHaddockVar prgIndex varInfo = let mPrg = Map.lookup varInfo.originatingMod prgIndex haddockIndex = maybe Map.empty (indexPrgHaddocks Map.empty) mPrg qualName = glblVarInfoToQualified varInfo in Map.lookup qualName haddockIndex +getRequiredHaddockName :: ProgramIndex -> GlblNameInfo -> Maybe HaddockInfo +getRequiredHaddockName prgIndex nameInfo = + let mPrg = Map.lookup nameInfo.originatingMod prgIndex + haddockIndex = maybe Map.empty (indexPrgHaddocks Map.empty) mPrg + qualName = glblNameInfoToQualified nameInfo + in Map.lookup qualName haddockIndex + ------------------- -- Definition ------------------- @@ -82,7 +90,7 @@ modLocToFileLcRange modFileMap (modText, lcRange) = do else pure Nothing pure pathList -resolvedToFileLcRange :: (MonadIO m) => ModFileMap -> Hir.ModuleText -> Resolveable -> m [FileLcRange] +resolvedToFileLcRange :: (MonadIO m) => ModFileMap -> Hir.ModuleText -> ResolveableRename -> m [FileLcRange] resolvedToFileLcRange modFileMap thisMod resolved = do let locLst = case resolved of Inj @(H.Variable RenamePhase) var -> maybe [] (resolvedLocs thisMod) var.ext @@ -112,7 +120,7 @@ resolvedVarToContents :: ProgramIndex -> ResolvedVariable -> Maybe Text resolvedVarToContents prgIndex resolvedVar = case resolvedVar of ResolvedVariable (ResolvedGlobal glblVarInfo) -> - let mHover = getRequiredHaddock prgIndex glblVarInfo + let mHover = getRequiredHaddockVar prgIndex glblVarInfo in Just $ renderGlblVarInfo mHover glblVarInfo _ -> Nothing @@ -138,3 +146,53 @@ renderGlblVarInfo mHaddock glblVarInfo = maybe "" (.node.dynNode.nodeText) glblVarInfo.sig wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" +nameToHover :: ProgramIndex -> (H.Name RenamePhase) -> Maybe Hover +nameToHover prgIndex nameNode = + let mResolvedName = nameNode.ext + range = ProtoLSP.lineColRangeToProto nameNode.dynNode.nodeLineColRange + mContents = (resolvedNameToContents prgIndex =<< mResolvedName) + in ( \contents -> + Hover + { _range = Just range + , _contents = InL $ MarkupContent MarkupKind_Markdown contents + } + ) + <$> mContents + +resolvedNameToContents :: ProgramIndex -> ResolvedName -> Maybe Text +resolvedNameToContents prgIndex resolvedName = + case resolvedName of + ResolvedName nameInfo _ -> + let mHover = getRequiredHaddockName prgIndex nameInfo + in Just $ renderNameInfo mHover nameInfo + _ -> Nothing + + +renderNameInfo :: Maybe HaddockInfo -> GlblNameInfo -> Text +renderNameInfo mHaddock nameInfo = + wrapHaskell + ( T.intercalate + "\n" + [ haddock, + declText + ] + ) + <> " \n\n**Type**: *" + <> nameKindText nameInfo.nameKind + <> "*" + <> " \n\nimported from: *" + <> T.intercalate ", " (NE.toList $ (.mod.text) <$> NESet.toList nameInfo.importedFrom) + <> "*" + <> " \noriginates from: *" + <> nameInfo.originatingMod.text + <> "*" + where + haddock = + maybe "" (.text) mHaddock + declText = pack (show nameInfo.decl) + wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" + + nameKindText :: NameKind -> Text + nameKindText DataDecl = "Data Type" + nameKindText NewtypeDecl = "Newtype" + nameKindText ClassDecl = "Type Class" \ No newline at end of file diff --git a/src/StaticLS/IDE/Hover.hs b/src/StaticLS/IDE/Hover.hs index b81bc45c..24708105 100644 --- a/src/StaticLS/IDE/Hover.hs +++ b/src/StaticLS/IDE/Hover.hs @@ -62,7 +62,7 @@ retrieveHover path lineCol = do (mResolved, prgs) <- getResolvedTermAndPrgs prg lineCol let astResult = case mResolved of Just (Inj @(H.Variable RenamePhase) var) -> varToHover prgs var - Just (Inj @(H.Name RenamePhase) _) -> Nothing + Just (Inj @(H.Name RenamePhase) name) -> nameToHover prgs name _ -> Nothing hieResult <- runMaybeT $ do hieFile <- getHieFile path From d1734f808e596bade0a131b8fda144d8de1dd690 Mon Sep 17 00:00:00 2001 From: Joseph Sumabat Date: Mon, 16 Jun 2025 11:56:49 -0400 Subject: [PATCH 12/21] Check ghciwatch is still compiling --- src/StaticLS/Handlers.hs | 18 +++++++++-- .../IDE/CodeActions/RemoveRedundantImports.hs | 2 +- src/StaticLS/IDE/Diagnostics/ParseGHC.hs | 31 +++++++++++++++++-- test/StaticLS/IDE/Diagnostics/ParseGHCSpec.hs | 3 ++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/StaticLS/Handlers.hs b/src/StaticLS/Handlers.hs index 82b3c012..18dad2e5 100644 --- a/src/StaticLS/Handlers.hs +++ b/src/StaticLS/Handlers.hs @@ -22,9 +22,10 @@ import Language.LSP.Protocol.Types qualified as LSP import Language.LSP.Server ( Handlers, LspT, + getVirtualFiles, ) import Language.LSP.Server qualified as LSP -import Language.LSP.VFS (VirtualFile (..)) +import Language.LSP.VFS (VirtualFile (..), VFS(..)) import StaticLS.Arborist import StaticLS.GhcidSession import StaticLS.HIE.File qualified as HIE.File @@ -51,10 +52,17 @@ import StaticLS.ProtoLSP qualified as ProtoLSP import StaticLS.StaticEnv qualified as StaticEnv import StaticLS.StaticEnv.Options (StaticEnvOptions (..)) import StaticLS.Utils -import System.Directory (doesFileExist) +import System.Directory (doesFileExist, getModificationTime) import System.FSNotify qualified as FSNotify import Text.Parsec.Text qualified as Parsec import UnliftIO.Exception qualified as Exception +import Data.Ord (comparing) +import Data.List (maximumBy) +import Data.Time.Clock (UTCTime) +import Data.IORef (readIORef) +import Data.ConcurrentCache +import qualified Data.Map.Strict as Map +import qualified Data.Text.Mixed.Rope as MixedRope ----------------------------------------------------------------- --------------------- LSP event handlers ------------------------ @@ -334,7 +342,11 @@ handleGhcidFileChange = do pure (staticEnv.wsRoot Path.) Right ghcid_session -> pure (ghcid_session.workingDirectory Path.) else pure (staticEnv.wsRoot Path.) - let diags = IDE.Diagnostics.ParseGHC.parse pathPrefix contents + let diags = + IDE.Diagnostics.ParseGHC.parse + pathPrefix + IDE.Diagnostics.ParseGHC.mainFile + contents lift $ logInfo $ "diags: " <> T.pack (show diags) clearDiagnostics sendDiagnostics Nothing diags diff --git a/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs b/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs index a7792c85..6d551e37 100644 --- a/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs +++ b/src/StaticLS/IDE/CodeActions/RemoveRedundantImports.hs @@ -58,7 +58,7 @@ getDiagnostics = do let makeAbsPath = unsafeFilePathToAbs . (wsRootPath System.FilePath.) . toFilePath let ghcidPath = wsRootPath System.FilePath. ghcidFile info <- liftIO $ catch @IOException (TextIO.readFile ghcidPath) (const $ pure "") - let diagnostics = parse makeAbsPath info + let diagnostics = parse makeAbsPath mainFile info pure diagnostics data DeletionInfo = Partial PartialDeletionInfo | Full FullDeletionInfo diff --git a/src/StaticLS/IDE/Diagnostics/ParseGHC.hs b/src/StaticLS/IDE/Diagnostics/ParseGHC.hs index eb4453ff..129e640b 100644 --- a/src/StaticLS/IDE/Diagnostics/ParseGHC.hs +++ b/src/StaticLS/IDE/Diagnostics/ParseGHC.hs @@ -2,9 +2,11 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE QuasiQuotes #-} +-- This file parses the output of ghciwatch or ghc diagnostics module StaticLS.IDE.Diagnostics.ParseGHC ( split, parse, + mainFile, parseErrorInfo, emptyErrorInfo, ErrorInfo (..), @@ -32,6 +34,9 @@ import StaticLS.IDE.FileWith (FileWith' (..)) import StaticLS.IDE.FileWith qualified as FileWith import Text.RawString.QQ import Text.Regex.TDFA qualified as RE +import Data.Ord (comparing) +import Data.List (maximumBy) +import qualified Data.HashMap.Strict as HashMap mkRegex :: Text -> RE.Regex mkRegex = RE.makeRegex @@ -203,8 +208,30 @@ toDiagnostic toAbs ((range, severity, info), message) = code = info.errorCode codeUri = fmap ("https://errors.haskell.org/messages/" <>) info.errorCode -parse :: (Path.RelPath -> Path.AbsPath) -> Text -> [Diagnostic] -parse toAbs = fmap (toDiagnostic toAbs . second (dedent . clean)) . split +isCompilingLine :: Text -> Bool +isCompilingLine t = t == "[ghciwatch is still compiling]" + +createCompilingDiagnostic :: (Path.RelPath -> Path.AbsPath) -> Path.RelPath -> Diagnostic +createCompilingDiagnostic toAbs mainFile = + Diagnostics.mkDiagnostic + (FileWith + { path = toAbs mainFile + , loc = LineColRange.point (LineCol (Pos 0) (Pos 0)) + }) + Diagnostics.Error + "ghciwatch is still compiling. Please wait for updated diagnostics" + +-- TODO: make configurable +mainFile :: Path.RelPath +mainFile = Path.filePathToRel "app/main.hs" + +parse :: (Path.RelPath -> Path.AbsPath) -> Path.RelPath -> Text -> [Diagnostic] +parse toAbs mainFile input = + let diags = fmap (toDiagnostic toAbs . second (dedent . clean)) . split $ input in + -- Special ghciwatch cas + if any isCompilingLine (T.lines input) + then createCompilingDiagnostic toAbs mainFile : diags + else diags dedent :: [Text] -> [Text] dedent lines = diff --git a/test/StaticLS/IDE/Diagnostics/ParseGHCSpec.hs b/test/StaticLS/IDE/Diagnostics/ParseGHCSpec.hs index 7198af42..ceb94820 100644 --- a/test/StaticLS/IDE/Diagnostics/ParseGHCSpec.hs +++ b/test/StaticLS/IDE/Diagnostics/ParseGHCSpec.hs @@ -41,12 +41,14 @@ spec = do it "weird case" do parse Path.uncheckedCoercePath + mainFile msg `shouldBe` [] it "smoke2" do parse Path.uncheckedCoercePath + mainFile [trimming| All good (60 modules) src/StaticLS/IDE/Diagnostics/ParseGHC.hs:18:1: warning: [-Wunused-imports] @@ -76,6 +78,7 @@ spec = do parse Path.uncheckedCoercePath + mainFile [trimming| src/StaticLS/PositionDiff.hs:19:3: warning: [GHC-47854] [-Wduplicate-exports] ‘getDiffMapFromDiff’ is exported by ‘getDiffMapFromDiff’ and ‘getDiffMapFromDiff’ From e92d494ec101ce4dfec8976a69d8159b2c21a324 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Tue, 17 Jun 2025 15:20:20 -0500 Subject: [PATCH 13/21] feat: go to definiton on constructors --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- src/StaticLS/Arborist.hs | 1 + src/StaticLS/IDE/Definition.hs | 5 ++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index 15707cee..656cd272 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 5477e35ab1f44e3d1fc15225bd478bebce02c173 + tag: 4d63f1b8b9aa08c44b9659e5a68f73a74e7c3e8d source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 38c41031..93dcd5b2 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-+k0Xpj1D6sQec0C4JU+rri86i5KYdCI+3p6O4QPuXiQ="; - rev = "5477e35ab1f44e3d1fc15225bd478bebce02c173"; + sha256 = "sha256-mbDnT+V7ILhp+RwHiybnSalPIZWdEQztIdiB97WlgKk="; + rev = "4d63f1b8b9aa08c44b9659e5a68f73a74e7c3e8d"; fetchSubmodules = true; }; diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 61c75ec2..68ae2549 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -95,6 +95,7 @@ resolvedToFileLcRange modFileMap thisMod resolved = do let locLst = case resolved of Inj @(H.Variable RenamePhase) var -> maybe [] (resolvedLocs thisMod) var.ext Inj @(H.Name RenamePhase) name -> maybe [] resolvedNameLocs name.ext + Inj @(H.Constructor RenamePhase) constructor -> maybe [] resolvedConstructorLocs constructor.ext _ -> [] fileLcRanges <- Monad.join <$> mapM (modLocToFileLcRange modFileMap) locLst pure fileLcRanges diff --git a/src/StaticLS/IDE/Definition.hs b/src/StaticLS/IDE/Definition.hs index 759186b9..3a0c2485 100644 --- a/src/StaticLS/IDE/Definition.hs +++ b/src/StaticLS/IDE/Definition.hs @@ -74,9 +74,12 @@ getArboristDefinition path lineCol = do case prg.mod of Just modText -> resolvedToFileLcRange modFileMap modText resolved Nothing -> pure [] + Just resolved@(Inj @(H.Constructor RenamePhase) _) -> + case prg.mod of + Just modText -> resolvedToFileLcRange modFileMap modText resolved + Nothing -> pure [] _ -> pure [] - getHieDefinition :: (MonadIde m, MonadIO m) => AbsPath -> From f27af8025bfa56b7616e03f195d4502ebbc48f81 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Fri, 20 Jun 2025 14:54:20 -0500 Subject: [PATCH 14/21] fix: fixed hover for names --- cabal.project | 12 ++++++------ nix/overlays/default.nix | 8 ++++---- src/StaticLS/Arborist.hs | 19 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/cabal.project b/cabal.project index 656cd272..358125c6 100644 --- a/cabal.project +++ b/cabal.project @@ -8,37 +8,37 @@ packages: source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea + tag: d388baf38d5548469f805dd80f004ca6de26cc24 subdir: tree-sitter-simple source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea + tag: d388baf38d5548469f805dd80f004ca6de26cc24 subdir: tree-sitter-haskell source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea + tag: d388baf38d5548469f805dd80f004ca6de26cc24 subdir: tree-sitter-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea + tag: d388baf38d5548469f805dd80f004ca6de26cc24 subdir: haskell-ast source-repository-package type: git location: https://github.com/josephsumabat/tree-sitter-simple.git - tag: 9ba9cdf3f678b1f5e68258247c33f6788bca6cea + tag: d388baf38d5548469f805dd80f004ca6de26cc24 subdir: text-range source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 4d63f1b8b9aa08c44b9659e5a68f73a74e7c3e8d + tag: d84603da0c404b1e498dfef8ad0fbcb70dbab9f3 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 93dcd5b2..c706cb1e 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -2,15 +2,15 @@ let tree-sitter-simple-repo = { url = "https://github.com/josephsumabat/tree-sitter-simple"; - sha256 = "sha256-un4lP1i/Xr4BnzHvyvFOlPs6kPqRA8tVwFZrQNY0+z0="; - rev = "9ba9cdf3f678b1f5e68258247c33f6788bca6cea"; + sha256 = "sha256-EpSbOb2+w/EOvr3nWQinA4lorv/Tqpz37LtngMLjEAM="; + rev = "d388baf38d5548469f805dd80f004ca6de26cc24"; fetchSubmodules = true; }; haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-mbDnT+V7ILhp+RwHiybnSalPIZWdEQztIdiB97WlgKk="; - rev = "4d63f1b8b9aa08c44b9659e5a68f73a74e7c3e8d"; + sha256 = "sha256-+jdlDU2wPhxBioI6yIB1N300IF8Wf84Q3OYkrUVDleQ="; + rev = "d84603da0c404b1e498dfef8ad0fbcb70dbab9f3"; fetchSubmodules = true; }; diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index 68ae2549..f358a972 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -23,6 +23,7 @@ import Data.Text qualified as T import Data.Time import Debug.Trace import Hir.Types qualified as Hir +import Hir.Types import Language.LSP.Protocol.Types import StaticLS.IDE.FileWith import StaticLS.IDE.Monad @@ -178,10 +179,7 @@ renderNameInfo mHaddock nameInfo = declText ] ) - <> " \n\n**Type**: *" - <> nameKindText nameInfo.nameKind - <> "*" - <> " \n\nimported from: *" + <> " \n\nimporated from: *" <> T.intercalate ", " (NE.toList $ (.mod.text) <$> NESet.toList nameInfo.importedFrom) <> "*" <> " \noriginates from: *" @@ -190,10 +188,11 @@ renderNameInfo mHaddock nameInfo = where haddock = maybe "" (.text) mHaddock - declText = pack (show nameInfo.decl) + declText = case nameInfo.decl of + DeclData decl -> decl.node.dynNode.nodeText + DeclNewtype decl -> decl.node.dynNode.nodeText + DeclClass decl -> decl.node.dynNode.nodeText + _ -> "Not supported yet." wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" - - nameKindText :: NameKind -> Text - nameKindText DataDecl = "Data Type" - nameKindText NewtypeDecl = "Newtype" - nameKindText ClassDecl = "Type Class" \ No newline at end of file + + From 3d01abd06cf442801558695572afa13291a67411 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Mon, 23 Jun 2025 12:09:34 -0700 Subject: [PATCH 15/21] fix: update links to haskell-arborist --- cabal.project | 2 +- nix/overlays/default.nix | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 358125c6..e1705f6a 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: d84603da0c404b1e498dfef8ad0fbcb70dbab9f3 + tag: 46da5dea9adcc9b525f1e20ba83d9597f27b8d58 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index c706cb1e..bc03ea9e 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-+jdlDU2wPhxBioI6yIB1N300IF8Wf84Q3OYkrUVDleQ="; - rev = "d84603da0c404b1e498dfef8ad0fbcb70dbab9f3"; + sha256 = "sha256-GDTkAjnY/rAaF4lyFkngjbOP/r/szvuFt7SN8F1YFAE="; + rev = "46da5dea9adcc9b525f1e20ba83d9597f27b8d58"; fetchSubmodules = true; }; From 42a985c6c4bd30e3e294dd534d474708a253dd2d Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Mon, 23 Jun 2025 15:07:40 -0700 Subject: [PATCH 16/21] fix: added hover for typeSynonyms --- src/StaticLS/Arborist.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticLS/Arborist.hs b/src/StaticLS/Arborist.hs index f358a972..368568e2 100644 --- a/src/StaticLS/Arborist.hs +++ b/src/StaticLS/Arborist.hs @@ -192,7 +192,8 @@ renderNameInfo mHaddock nameInfo = DeclData decl -> decl.node.dynNode.nodeText DeclNewtype decl -> decl.node.dynNode.nodeText DeclClass decl -> decl.node.dynNode.nodeText - _ -> "Not supported yet." + DeclTypeSynonym decl -> decl.node.dynNode.nodeText + _ -> "" wrapHaskell x = "\n```haskell\n" <> x <> "\n```\n" From 7907184b4c82a06479aeaf411ffe9978b1dee0be Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Thu, 10 Jul 2025 14:52:11 -0500 Subject: [PATCH 17/21] feat: static-ls autoimport --- cabal.project | 2 +- nix/overlays/default.nix | 4 +- src/StaticLS/IDE/CodeActions.hs | 4 +- .../IDE/CodeActions/AutoImportExisting.hs | 151 ++++++++++++++++++ static-ls.cabal | 1 + 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/StaticLS/IDE/CodeActions/AutoImportExisting.hs diff --git a/cabal.project b/cabal.project index e1705f6a..26fcb2bc 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: 46da5dea9adcc9b525f1e20ba83d9597f27b8d58 + tag: ef7a9f70ae5aea124ca913fdb439a4047d2dbb1f source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index bc03ea9e..eed5c203 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -9,8 +9,8 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; - sha256 = "sha256-GDTkAjnY/rAaF4lyFkngjbOP/r/szvuFt7SN8F1YFAE="; - rev = "46da5dea9adcc9b525f1e20ba83d9597f27b8d58"; + sha256 = "sha256-i9RechtvGAFvR1JxOCisWAFVCK2TYnTMVxg3ak9RWJk="; + rev = "ef7a9f70ae5aea124ca913fdb439a4047d2dbb1f"; fetchSubmodules = true; }; diff --git a/src/StaticLS/IDE/CodeActions.hs b/src/StaticLS/IDE/CodeActions.hs index 2ff4115d..929d8562 100644 --- a/src/StaticLS/IDE/CodeActions.hs +++ b/src/StaticLS/IDE/CodeActions.hs @@ -11,6 +11,7 @@ import Data.Rope qualified as Rope import StaticLS.IDE.CodeActions.AddTypeSig qualified as AddTypeSig import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.AutoImport qualified as AutoImport +import StaticLS.IDE.CodeActions.AutoImportExisting qualified as AutoImportExisting import StaticLS.IDE.CodeActions.RemoveRedundantImports as RemoveRedundantImports import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad @@ -27,7 +28,8 @@ getCodeActions path lineCol = do importCodeActions <- AutoImport.codeAction cx removeRedundantImports <- RemoveRedundantImports.codeAction cx exportCodeActions <- AutoExport.codeAction cx - let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions + autoImportExistingCodeActions <- AutoImportExisting.codeAction cx + let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions ++ autoImportExistingCodeActions pure codeActions resolveLazyAssist :: CodeActionMessage -> StaticLsM SourceEdit diff --git a/src/StaticLS/IDE/CodeActions/AutoImportExisting.hs b/src/StaticLS/IDE/CodeActions/AutoImportExisting.hs new file mode 100644 index 00000000..187b3d9f --- /dev/null +++ b/src/StaticLS/IDE/CodeActions/AutoImportExisting.hs @@ -0,0 +1,151 @@ +module StaticLS.IDE.CodeActions.AutoImportExisting where + +import AST qualified +import AST.Haskell as Haskell +import Arborist.AutoImport (addDeclToImportEdit) +import Arborist.Scope.Global (getGlobalAvalibleDecls) +import Arborist.Scope.Types (GlblDeclInfo(..)) +import Control.Monad (forM) +import Data.HashMap.Lazy qualified as Map +import Data.Path +import Data.Range (Range) +import Data.Range qualified as Range +import Data.Text (Text) +import Data.Either.Extra (eitherToMaybe) +import Hir.Parse as AST +import Hir.Types (ModuleText(..)) +import Hir.Types qualified as Hir +import StaticLS.IDE.CodeActions.Types +import StaticLS.IDE.Monad +import StaticLS.IDE.SourceEdit as SourceEdit +import StaticLS.Monad + +-- get the identifier at the cursor position +getIdentifierAtPoint :: Range -> Hir.Program -> Maybe (Maybe Text, Text) +getIdentifierAtPoint range prog = + case AST.getQualifiedAtPoint range prog.node of + Right (Just qualified) -> + let name = AST.nodeToText qualified.name.node + qualifier = case qualified.mod of + Nothing -> Nothing + Just modName -> Just (modName.mod.text) + in Just (qualifier, name) + _ -> Nothing + +-- find which modules an identifier can be imported from +findModulesForIdentifier :: AbsPath -> Text -> StaticLsM [(Text, Hir.Decl)] +findModulesForIdentifier path identifier = do + hir <- getHir path + + programIndex <- getPrgIndex + let exportIndex = Map.empty + + -- get all available declarations + let availableDecls = getGlobalAvalibleDecls programIndex exportIndex hir + matchingDecls = filter (\info -> info.name == identifier) availableDecls + moduleDecls = Map.toList $ Map.fromList + [(info.originatingMod.text, info.decl) | info <- matchingDecls] + + pure moduleDecls + +parseImportToHir :: Haskell.ImportP -> Maybe Hir.Import +parseImportToHir = eitherToMaybe . AST.parseImport + +-- find existing imports for a module in the current file +findExistingImports :: Text -> Maybe Text -> [(Haskell.ImportP, Hir.Import)] -> [(Haskell.ImportP, Hir.Import)] +findExistingImports moduleName mQualifier imports = + filter moduleMatches imports + where + moduleMatches (_, hirImport) = + hirImport.mod.text == moduleName && + case mQualifier of + Nothing -> not hirImport.qualified + Just qual -> + hirImport.qualified && + case hirImport.alias of + Just alias -> alias.text == qual + Nothing -> hirImport.mod.text == qual + +-- get all imports from the current file +getCurrentImports :: Hir.Program -> [Haskell.ImportP] +getCurrentImports prog = + let dynNode = AST.getDynNode prog.node + in getAllImports dynNode + where + getAllImports node = + let childImports = concatMap getAllImports node.nodeChildren + thisImport = case AST.cast @Haskell.ImportP node of + Just imp -> [imp] + Nothing -> [] + in thisImport ++ childImports + +-- check if identifier is already accessible through any import for this module +isIdentifierAccessible :: Text -> Text -> Maybe Text -> [(Haskell.ImportP, Hir.Import)] -> Bool +isIdentifierAccessible moduleName identifier mQualifier imports = + any canAccess imports + where + canAccess (_, hirImport) + | hirImport.mod.text /= moduleName = False + | hirImport.hiding = False + | otherwise = + case mQualifier of + Nothing -> + not hirImport.qualified && + case hirImport.importList of + Nothing -> True + Just [] -> False + Just items -> any (\item -> AST.nodeToText item.name.node == identifier) items + Just qual -> + hirImport.qualified && + matchesQualifier qual hirImport && + case hirImport.importList of + Nothing -> True + Just [] -> False + Just items -> any (\item -> AST.nodeToText item.name.node == identifier) items + + matchesQualifier qual hirImport = + case hirImport.alias of + Just alias -> alias.text == qual + Nothing -> hirImport.mod.text == qual + +-- check if identifier is in a specific import +identifierInImport :: Text -> Hir.Import -> Bool +identifierInImport identifier hirImport = + case hirImport.importList of + Nothing -> not hirImport.hiding + Just [] -> False + Just items -> any (\item -> AST.nodeToText item.name.node == identifier) items + +-- create assist for adding import +mkAssistForImport :: AbsPath -> Text -> Text -> Hir.Decl -> Haskell.ImportP -> Hir.Import -> Assist +mkAssistForImport path identifier moduleName decl existingImport hirImport = + let + dynNode = AST.getDynNode existingImport + importEdit = addDeclToImportEdit dynNode hirImport decl + sourceEdit = SourceEdit.single path importEdit + label = "Import " <> identifier <> " from " <> moduleName + in mkAssist label sourceEdit + +codeAction :: CodeActionContext -> StaticLsM [Assist] +codeAction CodeActionContext {path, pos} = do + hir <- getHir path + + let cursorLocation = Range.point pos + astImports = getCurrentImports hir + parsedImports = [(imp, hirImp) | imp <- astImports, Just hirImp <- [parseImportToHir imp]] + + -- get identifier at cursor position + case getIdentifierAtPoint cursorLocation hir of + Nothing -> pure [] + Just (mQualifier, identifier) -> do + moduleInfos <- findModulesForIdentifier path identifier + -- fore each module that exports this identifier + assists <- forM moduleInfos $ \(moduleName, decl) -> do + -- check if identifier is already accessible + if isIdentifierAccessible moduleName identifier mQualifier parsedImports + then pure [] + else do + let matchingImports = findExistingImports moduleName mQualifier parsedImports + importsNeedingAdd = filter (\(_, hirImp) -> not (identifierInImport identifier hirImp)) matchingImports + pure $ map (\(imp, hirImp) -> mkAssistForImport path identifier moduleName decl imp hirImp) importsNeedingAdd + pure $ concat assists \ No newline at end of file diff --git a/static-ls.cabal b/static-ls.cabal index 45fa4a84..f3e5d064 100644 --- a/static-ls.cabal +++ b/static-ls.cabal @@ -129,6 +129,7 @@ library StaticLS.IDE.CodeActions.AddTypeSig StaticLS.IDE.CodeActions.AutoExport StaticLS.IDE.CodeActions.AutoImport + StaticLS.IDE.CodeActions.AutoImportExisting StaticLS.IDE.CodeActions.RemoveRedundantImports StaticLS.IDE.CodeActions.Types StaticLS.IDE.Completion From b963a86aad140136b7e57096172a9ddd1e6d6197 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Fri, 11 Jul 2025 17:18:06 -0500 Subject: [PATCH 18/21] feat: testing for AutoImporExisting --- .../IDE/CodeActions/AutoImportExistingSpec.hs | 174 ++++++++++++++++++ test/StaticLS/IDE/CodeActions/TestUtils.hs | 28 +++ test/TestData/AutoImportExisting/First.hs | 26 +++ test/TestData/AutoImportExisting/Second.hs | 29 +++ test/TestData/AutoImportExisting/Third.hs | 29 +++ 5 files changed, 286 insertions(+) create mode 100644 test/StaticLS/IDE/CodeActions/AutoImportExistingSpec.hs create mode 100644 test/TestData/AutoImportExisting/First.hs create mode 100644 test/TestData/AutoImportExisting/Second.hs create mode 100644 test/TestData/AutoImportExisting/Third.hs diff --git a/test/StaticLS/IDE/CodeActions/AutoImportExistingSpec.hs b/test/StaticLS/IDE/CodeActions/AutoImportExistingSpec.hs new file mode 100644 index 00000000..0212a3b4 --- /dev/null +++ b/test/StaticLS/IDE/CodeActions/AutoImportExistingSpec.hs @@ -0,0 +1,174 @@ +module StaticLS.IDE.CodeActions.AutoImportExistingSpec (spec) where + +import Data.Path qualified as Path +import Data.Pos (Pos(..)) +import Data.LineCol (LineCol(..)) +import Data.Rope qualified as Rope +import Data.Text.IO qualified as T +import StaticLS.IDE.CodeActions.AutoImportExisting qualified as CodeActions.AutoImportExisting +import StaticLS.IDE.CodeActions.Types (CodeActionContext(..), Assist(..)) +import TestImport +import Test.Hspec +import Control.Monad.IO.Class (liftIO) +import StaticLS.Monad +import StaticLS.IDE.CodeActions.TestUtils qualified as TestUtils +import Control.Monad.Trans.Reader + +makePrgIndex :: ReaderT Env IO () +makePrgIndex = TestUtils.updatePrgIndex [ "test/TestData/AutoImportExisting/First.hs"] + +spec :: Spec +spec = do + describe "AutoImportExisting code action" $ do + + it "adds identifier to existing import list" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 453) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let Assist { label = assistLabel } = head assists + assistLabel `shouldBe` "Import bar from TestData.AutoImportExisting.Second" + + it "adds to qualified import with empty list" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 607) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let Assist { label = assistLabel } = head assists + assistLabel `shouldBe` "Import MyData from TestData.AutoImportExisting.Second" + + it "adds unqualified identifier to unqualified import" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 623) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + length assists `shouldBe` 1 + let labels = map (\(Assist { label }) -> label) assists + labels `shouldContain` ["Import foo from TestData.AutoImportExisting.Second"] + + it "adds operator to existing import" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 641) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let labels = map (\(Assist { label }) -> label) assists + (`elem` labels) "Import *** from TestData.AutoImportExisting.Second" `shouldBe` True + + it "adds qualified operator to qualified import" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 678) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let Assist { label = assistLabel } = head assists + assistLabel `shouldBe` "Import *** from TestData.AutoImportExisting.Second" + + it "failts to add type to open qualified import with alias" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 701) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` null + + it "handles fully qualified import" $ do + firstPath <- Path.filePathToAbs "test/TestData/AutoImportExisting/First.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 583) + ctx = CodeActionContext + { path = firstPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoImportExisting.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let Assist { label = assistLabel } = head assists + assistLabel `shouldBe` "Import MyNewtype from TestData.AutoImportExisting.Third" \ No newline at end of file diff --git a/test/StaticLS/IDE/CodeActions/TestUtils.hs b/test/StaticLS/IDE/CodeActions/TestUtils.hs index 4689fe5e..9b2ceab4 100644 --- a/test/StaticLS/IDE/CodeActions/TestUtils.hs +++ b/test/StaticLS/IDE/CodeActions/TestUtils.hs @@ -14,6 +14,14 @@ import StaticLS.IDE.SourceEdit (SourceEdit (..)) import StaticLS.Monad import StaticLS.Utils (isJustOrThrowS) import Test.Hspec +import Data.Text.IO qualified as T +import Data.HashMap.Internal qualified as HM +import Hir.Types qualified as Hir +import Control.Monad +import Hir.Parse qualified as Hir +import AST.Haskell qualified as AST +import Arborist.ProgramIndex +import Control.Monad.Trans.Reader checkCodeAction :: (HasCallStack) => @@ -49,3 +57,23 @@ checkCodeAction path pos codeAction findAssist = do liftIO $ Rope.toText rope' `shouldBe` expected pure () pure () + + +getPrg :: [FilePath] -> IO [Hir.Program] +getPrg hsFiles = + forM hsFiles $ \file -> do + fileContents <- T.readFile file + let v = Hir.parseHaskell (AST.parse fileContents) + pure $ snd v + +updatePrgIndex :: [FilePath] -> ReaderT Env IO () +updatePrgIndex file = do + programs <- liftIO $ getPrg file + let prog = head programs + modFileMap <- getModFileMap + + -- get sccope + scopeDeps <- liftIO $ gatherScopeDeps HM.empty prog modFileMap Nothing + + -- write new prg index + tryWritePrgIndex (const scopeDeps) \ No newline at end of file diff --git a/test/TestData/AutoImportExisting/First.hs b/test/TestData/AutoImportExisting/First.hs new file mode 100644 index 00000000..37604c1c --- /dev/null +++ b/test/TestData/AutoImportExisting/First.hs @@ -0,0 +1,26 @@ +module TestData.AutoImportExisting.First where + +import TestData.AutoImportExisting.Second (MyClass(..)) +import TestData.AutoImportExisting.Third () +import TestData.AutoImportExisting.Third +import TestData.AutoImportExisting.Second qualified as Q () +import qualified TestData.AutoImportExisting.Second as G () + +import qualified TestData.AutoImportExisting.Second as G +import qualified TestData.AutoImportExisting.Third () + +test :: String +test = foo ++ bar + +testFunc :: Int -> Int +testFunc x = + let a = MyClass + f = MyClass + b = TestData.AutoImportExisting.Third.MyNewtype + c = Q.MyData + d = foo + e = (***) + g = Q.foo + h = (Q.***) + i = G.MyNewtype + in x + 1 \ No newline at end of file diff --git a/test/TestData/AutoImportExisting/Second.hs b/test/TestData/AutoImportExisting/Second.hs new file mode 100644 index 00000000..ba640031 --- /dev/null +++ b/test/TestData/AutoImportExisting/Second.hs @@ -0,0 +1,29 @@ +module TestData.AutoImportExisting.Second where + +import Prelude + +class MyClass a where + myMethod :: a -> Bool + myMethod = undefined + +newtype MyNewtype = MyNewtype Int + +type MyType = Int + +data MyData + = MyDataConstructor Int String + +foo :: Int -> Int +foo x = x + 1 + +bar :: String +bar = "bar" + +(***) :: Int -> Int -> Int +(***) = (+) + +type family MyTypeFamily a :: * +type instance MyTypeFamily Int = Bool + +data family MyDataFamily a +data instance MyDataFamily Int = MyDataFamilyInt Int \ No newline at end of file diff --git a/test/TestData/AutoImportExisting/Third.hs b/test/TestData/AutoImportExisting/Third.hs new file mode 100644 index 00000000..48229f1c --- /dev/null +++ b/test/TestData/AutoImportExisting/Third.hs @@ -0,0 +1,29 @@ +module TestData.AutoImportExisting.Third where + +import Prelude + +class MyClass a where + myMethod :: a -> Bool + myMethod = undefined + +newtype MyNewtype = MyNewtype Int + +type MyType = Int + +data MyData + = MyDataConstructor Int String + +foo :: Int -> Int +foo x = x + 1 + +bar :: String +bar = "bar" + +(***) :: Int -> Int -> Int +(***) = (+) + +type family MyTypeFamily a :: * +type instance MyTypeFamily Int = Bool + +data family MyDataFamily a +data instance MyDataFamily Int = MyDataFamilyInt Int \ No newline at end of file From e837a56cccbf9a63b491f5e149b9507615abd013 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Fri, 11 Jul 2025 17:25:07 -0500 Subject: [PATCH 19/21] feat: update link to arborist --- cabal.project | 2 +- nix/overlays/default.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 26fcb2bc..3f4ee3af 100644 --- a/cabal.project +++ b/cabal.project @@ -38,7 +38,7 @@ source-repository-package source-repository-package type: git location: https://github.com/josephsumabat/haskell-arborist - tag: ef7a9f70ae5aea124ca913fdb439a4047d2dbb1f + tag: fcd4db468d7766d3cdb067eaa2ef1170275fee37 source-repository-package type: git diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index eed5c203..99582871 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -10,7 +10,7 @@ let haskell-arborist-repo = { url = "https://github.com/josephsumabat/haskell-arborist"; sha256 = "sha256-i9RechtvGAFvR1JxOCisWAFVCK2TYnTMVxg3ak9RWJk="; - rev = "ef7a9f70ae5aea124ca913fdb439a4047d2dbb1f"; + rev = "fcd4db468d7766d3cdb067eaa2ef1170275fee37"; fetchSubmodules = true; }; From 800d99480099a733b1a0cc8e9872a4148b375796 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Thu, 17 Jul 2025 12:35:32 -0500 Subject: [PATCH 20/21] feat: implement auto qualify --- src/StaticLS/IDE/CodeActions.hs | 4 +- src/StaticLS/IDE/CodeActions/AutoQualify.hs | 124 ++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/StaticLS/IDE/CodeActions/AutoQualify.hs diff --git a/src/StaticLS/IDE/CodeActions.hs b/src/StaticLS/IDE/CodeActions.hs index 929d8562..3f1ea523 100644 --- a/src/StaticLS/IDE/CodeActions.hs +++ b/src/StaticLS/IDE/CodeActions.hs @@ -12,6 +12,7 @@ import StaticLS.IDE.CodeActions.AddTypeSig qualified as AddTypeSig import StaticLS.IDE.CodeActions.AutoExport as AutoExport import StaticLS.IDE.CodeActions.AutoImport qualified as AutoImport import StaticLS.IDE.CodeActions.AutoImportExisting qualified as AutoImportExisting +import StaticLS.IDE.CodeActions.AutoQualify qualified as AutoQualify import StaticLS.IDE.CodeActions.RemoveRedundantImports as RemoveRedundantImports import StaticLS.IDE.CodeActions.Types import StaticLS.IDE.Monad @@ -29,7 +30,8 @@ getCodeActions path lineCol = do removeRedundantImports <- RemoveRedundantImports.codeAction cx exportCodeActions <- AutoExport.codeAction cx autoImportExistingCodeActions <- AutoImportExisting.codeAction cx - let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions ++ autoImportExistingCodeActions + autoQualifyCodeActions <- AutoQualify.codeAction cx + let codeActions = typesCodeActions ++ importCodeActions ++ removeRedundantImports ++ exportCodeActions ++ autoImportExistingCodeActions ++ autoQualifyCodeActions pure codeActions resolveLazyAssist :: CodeActionMessage -> StaticLsM SourceEdit diff --git a/src/StaticLS/IDE/CodeActions/AutoQualify.hs b/src/StaticLS/IDE/CodeActions/AutoQualify.hs new file mode 100644 index 00000000..7b0c312c --- /dev/null +++ b/src/StaticLS/IDE/CodeActions/AutoQualify.hs @@ -0,0 +1,124 @@ +module StaticLS.IDE.CodeActions.AutoQualify where + +import AST qualified +import AST.Haskell as Haskell +import Arborist.AutoQualify (qualifyIdentifier) +import Arborist.Scope.Global (getGlobalAvalibleDecls) +import Arborist.Scope.Types (GlblDeclInfo(..)) +import Control.Monad (forM) +import Data.HashMap.Lazy qualified as Map +import Data.Path +import Data.Range (Range) +import Data.Range qualified as Range +import Data.Text (Text) +import Data.Text qualified as T +import Data.Either.Extra (eitherToMaybe) +import Hir.Parse as AST +import Hir.Types (ModuleText(..)) +import Hir.Types qualified as Hir +import StaticLS.IDE.CodeActions.Types +import StaticLS.IDE.Monad +import StaticLS.IDE.SourceEdit as SourceEdit +import StaticLS.Monad +import Debug.Trace + +-- get the identifier at the cursor position +getIdentifierAtPoint :: Range -> Hir.Program -> Maybe (Maybe Text, Text) +getIdentifierAtPoint range prog = + case AST.getQualifiedAtPoint range prog.node of + Right (Just qualified) -> + let name = AST.nodeToText qualified.name.node + qualifier = case qualified.mod of + Nothing -> Nothing + Just modName -> Just (modName.mod.text) + in Just (qualifier, name) + _ -> Nothing + +-- find which modules an identifier can be imported from +findModulesForIdentifier :: AbsPath -> Text -> StaticLsM [(Text, Hir.Decl)] +findModulesForIdentifier path identifier = do + hir <- getHir path + + programIndex <- getPrgIndex + let exportIndex = Map.empty + + -- get all available declarations + let availableDecls = getGlobalAvalibleDecls programIndex exportIndex hir + matchingDecls = filter (\info -> info.name == identifier) availableDecls + moduleDecls = Map.toList $ Map.fromList + [(info.originatingMod.text, info.decl) | info <- matchingDecls] + + pure moduleDecls + +parseImportToHir :: Haskell.ImportP -> Maybe Hir.Import +parseImportToHir = eitherToMaybe . AST.parseImport + +-- get all imports from the current file +getCurrentImports :: Hir.Program -> [Haskell.ImportP] +getCurrentImports prog = + let dynNode = AST.getDynNode prog.node + in getAllImports dynNode + where + getAllImports node = + let childImports = concatMap getAllImports node.nodeChildren + thisImport = case AST.cast @Haskell.ImportP node of + Just imp -> [imp] + Nothing -> [] + in thisImport ++ childImports + +-- check if identifier is in an import +identifierInImport :: Text -> Hir.Import -> Bool +identifierInImport identifier hirImport = + case hirImport.importList of + Nothing -> not hirImport.hiding + Just [] -> False + Just items -> any (\item -> AST.nodeToText item.name.node == identifier) items + +-- find qualified imports that can provide this identifier +findQualifiedImportsForIdentifier :: AbsPath -> Text -> [(Haskell.ImportP, Hir.Import)] -> StaticLsM [(Haskell.ImportP, Hir.Import)] +findQualifiedImportsForIdentifier path identifier imports = do + -- get modules that actually export this identifier + modulesThatExport <- findModulesForIdentifier path identifier + let exportingModules = map fst modulesThatExport + pure $ filter (canProvideIdentifier exportingModules) imports + where + canProvideIdentifier exportingModules (_, hirImport) = + hirImport.qualified && + hirImport.mod.text `elem` exportingModules && + identifierInImport identifier hirImport + +-- create assist for qualifying +mkAssistForQualify :: AbsPath -> Text -> AST.DynNode -> Haskell.ImportP -> Hir.Import -> Assist +mkAssistForQualify path identifier usageNode importP hirImport = + let + qualifyEdit = qualifyIdentifier usageNode hirImport + sourceEdit = SourceEdit.single path qualifyEdit + qualifier = case hirImport.alias of + Just alias -> alias.text + Nothing -> hirImport.mod.text + label = "Qualify as " <> qualifier <> "." <> identifier + in mkAssist label sourceEdit + +codeAction :: CodeActionContext -> StaticLsM [Assist] +codeAction CodeActionContext {path, pos} = do + hir <- getHir path + + let cursorLocation = Range.point pos + astImports = getCurrentImports hir + parsedImports = [(imp, hirImp) | imp <- astImports, Just hirImp <- [parseImportToHir imp]] + + -- get identifier at cursor position + case getIdentifierAtPoint cursorLocation hir of + Nothing -> pure [] + Just (mQualifier, identifier) -> do + case mQualifier of + Just _ -> pure [] + Nothing -> do + case AST.getQualifiedAtPoint cursorLocation hir.node of + Right (Just qualified) -> do + let usageNode = qualified.name.node + qualifiedImports <- findQualifiedImportsForIdentifier path identifier parsedImports + + let assists = map (uncurry (mkAssistForQualify path identifier usageNode)) qualifiedImports + pure assists + _ -> pure [] \ No newline at end of file From cf5924527550ead96e42c97d24fbb415dee29e08 Mon Sep 17 00:00:00 2001 From: Shan Ali Date: Thu, 17 Jul 2025 12:35:44 -0500 Subject: [PATCH 21/21] feat: add testing suite for auto qualify --- .../IDE/CodeActions/AutoQualifySpec.hs | 108 ++++++++++++++++++ test/TestData/AutoQualify/ExportModule.hs | 17 +++ test/TestData/AutoQualify/TestFile.hs | 11 ++ 3 files changed, 136 insertions(+) create mode 100644 test/StaticLS/IDE/CodeActions/AutoQualifySpec.hs create mode 100644 test/TestData/AutoQualify/ExportModule.hs create mode 100644 test/TestData/AutoQualify/TestFile.hs diff --git a/test/StaticLS/IDE/CodeActions/AutoQualifySpec.hs b/test/StaticLS/IDE/CodeActions/AutoQualifySpec.hs new file mode 100644 index 00000000..606e06e5 --- /dev/null +++ b/test/StaticLS/IDE/CodeActions/AutoQualifySpec.hs @@ -0,0 +1,108 @@ +module StaticLS.IDE.CodeActions.AutoQualifySpec (spec) where + +import Data.Path qualified as Path +import Data.Pos (Pos(..)) +import Data.LineCol (LineCol(..)) +import StaticLS.IDE.CodeActions.AutoQualify qualified as CodeActions.AutoQualify +import StaticLS.IDE.CodeActions.Types (CodeActionContext(..), Assist(..)) +import TestImport +import Test.Hspec +import Control.Monad.IO.Class (liftIO) +import StaticLS.Monad +import StaticLS.IDE.CodeActions.TestUtils qualified as TestUtils +import Control.Monad.Trans.Reader + +makePrgIndex :: ReaderT Env IO () +makePrgIndex = TestUtils.updatePrgIndex ["test/TestData/AutoQualify/TestFile.hs"] + +spec :: Spec +spec = do + describe "AutoQualify code action" $ do + + it "qualifies foo with available aliases" $ do + testPath <- Path.filePathToAbs "test/TestData/AutoQualify/TestFile.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 302) + ctx = CodeActionContext + { path = testPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoQualify.codeAction ctx + pure result + + length assists `shouldBe` 3 + let labels = map (\(Assist { label }) -> label) assists + labels `shouldContain` ["Qualify as E.foo"] + labels `shouldContain` ["Qualify as Export.foo"] + labels `shouldContain` ["Qualify as Q.foo"] + + it "qualifies operator with available alias" $ do + testPath <- Path.filePathToAbs "test/TestData/AutoQualify/TestFile.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 338) + ctx = CodeActionContext + { path = testPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoQualify.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let labels = map (\(Assist { label }) -> label) assists + labels `shouldContain` ["Qualify as E.***"] + + it "does not qualify already qualified identifier" $ do + testPath <- Path.filePathToAbs "test/TestData/AutoQualify/TestFile.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 323) + ctx = CodeActionContext + { path = testPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoQualify.codeAction ctx + pure result + + assists `shouldSatisfy` null + + it "qualifies with full module name when no alias" $ do + testPath <- Path.filePathToAbs "test/TestData/AutoQualify/TestFile.hs" + + env <- initTestEnv + assists <- runStaticLsM env $ do + makePrgIndex + + let lineCol = LineCol (Pos 0) (Pos 0) + cursorPos = (Pos 351) + ctx = CodeActionContext + { path = testPath + , pos = cursorPos + , lineCol = lineCol + } + + result <- CodeActions.AutoQualify.codeAction ctx + pure result + + assists `shouldSatisfy` (not . null) + let labels = map (\(Assist { label }) -> label) assists + labels `shouldContain` ["Qualify as TestData.AutoQualify.ExportModule.MyData"] \ No newline at end of file diff --git a/test/TestData/AutoQualify/ExportModule.hs b/test/TestData/AutoQualify/ExportModule.hs new file mode 100644 index 00000000..5c9bd810 --- /dev/null +++ b/test/TestData/AutoQualify/ExportModule.hs @@ -0,0 +1,17 @@ +module TestData.AutoQualify.ExportModule + ( foo + , bar + , MyData(..) + , (***) + ) where + +foo :: Int -> Int +foo x = x + 1 + +bar :: String -> String +bar s = s ++ "!" + +data MyData = MyData Int String + +(***) :: Int -> Int -> Int +(***) = (+) \ No newline at end of file diff --git a/test/TestData/AutoQualify/TestFile.hs b/test/TestData/AutoQualify/TestFile.hs new file mode 100644 index 00000000..afd5cb23 --- /dev/null +++ b/test/TestData/AutoQualify/TestFile.hs @@ -0,0 +1,11 @@ +module TestData.AutoQualify.TestFile where + +import TestData.AutoQualify.ExportModule qualified as E +import TestData.AutoQualify.ExportModule qualified as Export (foo, bar) +import qualified TestData.AutoQualify.ExportModule as Q +import qualified TestData.AutoQualify.ExportModule (MyData(..)) + +test1 = foo 5 +test2 = bar "hello" +test3 = 1 *** 2 +test4 = MyData 1 "x" \ No newline at end of file