Skip to content

Commit f63f0bd

Browse files
committed
Add completion for import fields in cabal files
At the moment import fields always suggest any common stanza names occuring in the file, while it should be only the ones defined before the cursor position. Also moves all CabalFields utility into a separate module
1 parent e9c2f55 commit f63f0bd

File tree

9 files changed

+214
-72
lines changed

9 files changed

+214
-72
lines changed

haskell-language-server.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ library hls-cabal-plugin
231231
exposed-modules:
232232
Ide.Plugin.Cabal
233233
Ide.Plugin.Cabal.Diagnostics
234+
Ide.Plugin.Cabal.Completion.CabalFields
234235
Ide.Plugin.Cabal.Completion.Completer.FilePath
235236
Ide.Plugin.Cabal.Completion.Completer.Module
236237
Ide.Plugin.Cabal.Completion.Completer.Paths

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs

+14-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Data.Hashable
1818
import Data.HashMap.Strict (HashMap)
1919
import qualified Data.HashMap.Strict as HashMap
2020
import qualified Data.List.NonEmpty as NE
21+
import qualified Data.Maybe as Maybe
2122
import qualified Data.Text.Encoding as Encoding
2223
import Data.Typeable
2324
import Development.IDE as D
@@ -32,7 +33,8 @@ import qualified Distribution.Parsec.Position as Syntax
3233
import GHC.Generics
3334
import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes
3435
import qualified Ide.Plugin.Cabal.Completion.Completions as Completions
35-
import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..),
36+
import Ide.Plugin.Cabal.Completion.Types (ParseCabalCommonSections (ParseCabalCommonSections),
37+
ParseCabalFields (..),
3638
ParseCabalFile (..))
3739
import qualified Ide.Plugin.Cabal.Completion.Types as Types
3840
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
@@ -171,6 +173,14 @@ cabalRules recorder plId = do
171173
Right fields ->
172174
pure ([], Just fields)
173175

176+
define (cmapWithPrio LogShake recorder) $ \ParseCabalCommonSections file -> do
177+
fields <- use_ ParseCabalFields file
178+
let commonSections = Maybe.mapMaybe (\case
179+
commonSection@(Syntax.Section (Syntax.Name _ "common") _ _) -> Just commonSection
180+
_ -> Nothing)
181+
fields
182+
pure ([], Just commonSections)
183+
174184
define (cmapWithPrio LogShake recorder) $ \ParseCabalFile file -> do
175185
config <- getPluginConfigAction plId
176186
if not (plcGlobalOn config && plcDiagnosticsOn config)
@@ -337,6 +347,9 @@ completion recorder ide _ complParams = do
337347
{ getLatestGPD = do
338348
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast ParseCabalFile $ toNormalizedFilePath fp
339349
pure $ fmap fst mGPD
350+
, getCabalCommonSections = do
351+
mSections <- runIdeAction "cabal-plugin.modulesCompleter.commonsections" (shakeExtras ide) $ useWithStaleFast ParseCabalCommonSections $ toNormalizedFilePath fp
352+
pure $ fmap fst mSections
340353
, cabalPrefixInfo = prefInfo
341354
, stanzaName =
342355
case fst ctx of
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module Ide.Plugin.Cabal.Completion.CabalFields (findStanzaForColumn, findFieldSection, getOptionalSectionName, getAnnotation, getFieldName) where
2+
3+
import Data.List.NonEmpty (NonEmpty)
4+
import qualified Data.List.NonEmpty as NE
5+
import qualified Data.Text as T
6+
import qualified Data.Text.Encoding as T
7+
import qualified Distribution.Fields as Syntax
8+
import qualified Distribution.Parsec.Position as Syntax
9+
import Ide.Plugin.Cabal.Completion.Types
10+
11+
-- ----------------------------------------------------------------
12+
-- Cabal-syntax utilities I don't really want to write myself
13+
-- ----------------------------------------------------------------
14+
15+
-- | Determine the context of a cursor position within a stack of stanza contexts
16+
--
17+
-- If the cursor is indented more than one of the stanzas in the stack
18+
-- the respective stanza is returned if this is never the case, the toplevel stanza
19+
-- in the stack is returned.
20+
findStanzaForColumn :: Int -> NonEmpty (Int, StanzaContext) -> (StanzaContext, FieldContext)
21+
findStanzaForColumn col ctx = case NE.uncons ctx of
22+
((_, stanza), Nothing) -> (stanza, None)
23+
((indentation, stanza), Just res)
24+
| col < indentation -> findStanzaForColumn col res
25+
| otherwise -> (stanza, None)
26+
27+
-- | Determine the field the cursor is currently a part of.
28+
--
29+
-- The result is said field and its starting position
30+
-- or Nothing if the passed list of fields is empty.
31+
32+
-- This only looks at the row of the cursor and not at the cursor's
33+
-- position within the row.
34+
--
35+
-- TODO: we do not handle braces correctly. Add more tests!
36+
findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position)
37+
findFieldSection _cursor [] = Nothing
38+
findFieldSection _cursor [x] =
39+
-- Last field. We decide later, whether we are starting
40+
-- a new section.
41+
Just x
42+
findFieldSection cursor (x:y:ys)
43+
| Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y)
44+
= Just x
45+
| otherwise = findFieldSection cursor (y:ys)
46+
where
47+
cursorLine = Syntax.positionRow cursor
48+
49+
type FieldName = T.Text
50+
51+
getAnnotation :: Syntax.Field ann -> ann
52+
getAnnotation (Syntax.Field (Syntax.Name ann _) _) = ann
53+
getAnnotation (Syntax.Section (Syntax.Name ann _) _ _) = ann
54+
55+
getFieldName :: Syntax.Field ann -> FieldName
56+
getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn
57+
getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn
58+
59+
-- | Returns the name of a section if it has a name.
60+
--
61+
-- This assumes that the given section args belong to named stanza
62+
-- in which case the stanza name is returned.
63+
getOptionalSectionName :: [Syntax.SectionArg ann] -> Maybe T.Text
64+
getOptionalSectionName [] = Nothing
65+
getOptionalSectionName (x:xs) = case x of
66+
Syntax.SecArgName _ name -> Just (T.decodeUtf8 name)
67+
_ -> getOptionalSectionName xs
68+

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE LambdaCase #-}
12
{-# LANGUAGE OverloadedStrings #-}
23

34
module Ide.Plugin.Cabal.Completion.Completer.Simple where
@@ -7,11 +8,14 @@ import Data.Function ((&))
78
import qualified Data.List as List
89
import Data.Map (Map)
910
import qualified Data.Map as Map
10-
import Data.Maybe (fromMaybe)
11+
import Data.Maybe (fromMaybe,
12+
mapMaybe)
1113
import Data.Ord (Down (Down))
1214
import qualified Data.Text as T
15+
import qualified Distribution.Fields as Syntax
1316
import Ide.Logger (Priority (..),
1417
logWith)
18+
import Ide.Plugin.Cabal.Completion.CabalFields
1519
import Ide.Plugin.Cabal.Completion.Completer.Types
1620
import Ide.Plugin.Cabal.Completion.Types (CabalPrefixInfo (..),
1721
Log)
@@ -41,6 +45,22 @@ constantCompleter completions _ cData = do
4145
range = completionRange prefInfo
4246
pure $ map (mkSimpleCompletionItem range . Fuzzy.original) scored
4347

48+
-- | Completer to be used for import fields.
49+
--
50+
-- TODO: Does not exclude imports, defined after the current cursor position
51+
-- which are not allowed according to the cabal specification
52+
importCompleter :: Completer
53+
importCompleter l cData = do
54+
cabalCommonsM <- getCabalCommonSections cData
55+
case cabalCommonsM of
56+
Just cabalCommons -> do
57+
let commonNames = mapMaybe (\case
58+
Syntax.Section (Syntax.Name _ "common") commonNames _ -> getOptionalSectionName commonNames
59+
_ -> Nothing)
60+
cabalCommons
61+
constantCompleter commonNames l cData
62+
Nothing -> noopCompleter l cData
63+
4464
-- | Completer to be used for the field @name:@ value.
4565
--
4666
-- This is almost always the name of the cabal file. However,

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
module Ide.Plugin.Cabal.Completion.Completer.Types where
44

55
import Development.IDE as D
6+
import qualified Distribution.Fields as Syntax
67
import Distribution.PackageDescription (GenericPackageDescription)
8+
import qualified Distribution.Parsec.Position as Syntax
79
import Ide.Plugin.Cabal.Completion.Types
810
import Language.LSP.Protocol.Types (CompletionItem)
911

@@ -16,9 +18,11 @@ data CompleterData = CompleterData
1618
{ -- | Access to the latest available generic package description for the handled cabal file,
1719
-- relevant for some completion actions which require the file's meta information
1820
-- such as the module completers which require access to source directories
19-
getLatestGPD :: IO (Maybe GenericPackageDescription),
21+
getLatestGPD :: IO (Maybe GenericPackageDescription),
22+
-- | Access to the entries of the handled cabal file as parsed by ParseCabalFields
23+
getCabalCommonSections :: IO (Maybe [Syntax.Field Syntax.Position]),
2024
-- | Prefix info to be used for constructing completion items
21-
cabalPrefixInfo :: CabalPrefixInfo,
25+
cabalPrefixInfo :: CabalPrefixInfo,
2226
-- | The name of the stanza in which the completer is applied
23-
stanzaName :: Maybe StanzaName
27+
stanzaName :: Maybe StanzaName
2428
}

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs

+1-55
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import Data.List.NonEmpty (NonEmpty)
88
import qualified Data.List.NonEmpty as NE
99
import qualified Data.Map as Map
1010
import qualified Data.Text as T
11-
import qualified Data.Text.Encoding as T
1211
import Development.IDE as D
1312
import qualified Development.IDE.Plugin.Completions.Types as Ghcide
1413
import qualified Distribution.Fields as Syntax
1514
import qualified Distribution.Parsec.Position as Syntax
15+
import Ide.Plugin.Cabal.Completion.CabalFields
1616
import Ide.Plugin.Cabal.Completion.Completer.Simple
1717
import Ide.Plugin.Cabal.Completion.Completer.Snippet
1818
import Ide.Plugin.Cabal.Completion.Completer.Types (Completer)
@@ -177,57 +177,3 @@ classifyFieldContext ctx cursor field
177177

178178
cursorColumn = Syntax.positionCol cursor
179179
fieldColumn = Syntax.positionCol (getAnnotation field)
180-
181-
-- ----------------------------------------------------------------
182-
-- Cabal-syntax utilities I don't really want to write myself
183-
-- ----------------------------------------------------------------
184-
185-
-- | Determine the context of a cursor position within a stack of stanza contexts
186-
--
187-
-- If the cursor is indented more than one of the stanzas in the stack
188-
-- the respective stanza is returned if this is never the case, the toplevel stanza
189-
-- in the stack is returned.
190-
findStanzaForColumn :: Int -> NonEmpty (Int, StanzaContext) -> (StanzaContext, FieldContext)
191-
findStanzaForColumn col ctx = case NE.uncons ctx of
192-
((_, stanza), Nothing) -> (stanza, None)
193-
((indentation, stanza), Just res)
194-
| col < indentation -> findStanzaForColumn col res
195-
| otherwise -> (stanza, None)
196-
197-
-- | Determine the field the cursor is currently a part of.
198-
--
199-
-- The result is said field and its starting position
200-
-- or Nothing if the passed list of fields is empty.
201-
202-
-- This only looks at the row of the cursor and not at the cursor's
203-
-- position within the row.
204-
--
205-
-- TODO: we do not handle braces correctly. Add more tests!
206-
findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position)
207-
findFieldSection _cursor [] = Nothing
208-
findFieldSection _cursor [x] =
209-
-- Last field. We decide later, whether we are starting
210-
-- a new section.
211-
Just x
212-
findFieldSection cursor (x:y:ys)
213-
| Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y)
214-
= Just x
215-
| otherwise = findFieldSection cursor (y:ys)
216-
where
217-
cursorLine = Syntax.positionRow cursor
218-
219-
type FieldName = T.Text
220-
221-
getAnnotation :: Syntax.Field ann -> ann
222-
getAnnotation (Syntax.Field (Syntax.Name ann _) _) = ann
223-
getAnnotation (Syntax.Section (Syntax.Name ann _) _ _) = ann
224-
225-
getFieldName :: Syntax.Field ann -> FieldName
226-
getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn
227-
getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn
228-
229-
getOptionalSectionName :: [Syntax.SectionArg ann] -> Maybe T.Text
230-
getOptionalSectionName [] = Nothing
231-
getOptionalSectionName (x:xs) = case x of
232-
Syntax.SecArgName _ name -> Just (T.decodeUtf8 name)
233-
_ -> getOptionalSectionName xs

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs

+2-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ flagFields =
162162
libExecTestBenchCommons :: Map KeyWordName Completer
163163
libExecTestBenchCommons =
164164
Map.fromList
165-
[ ("build-depends:", noopCompleter),
165+
[ ("import:", importCompleter),
166+
("build-depends:", noopCompleter),
166167
("hs-source-dirs:", directoryCompleter),
167168
("default-extensions:", noopCompleter),
168169
("other-extensions:", noopCompleter),

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs

+9
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ instance Hashable ParseCabalFields
5959

6060
instance NFData ParseCabalFields
6161

62+
type instance RuleResult ParseCabalCommonSections = [Syntax.Field Syntax.Position]
63+
64+
data ParseCabalCommonSections = ParseCabalCommonSections
65+
deriving (Eq, Show, Typeable, Generic)
66+
67+
instance Hashable ParseCabalCommonSections
68+
69+
instance NFData ParseCabalCommonSections
70+
6271
-- | The context a cursor can be in within a cabal file.
6372
--
6473
-- We can be in stanzas or the top level,

0 commit comments

Comments
 (0)