Skip to content

Commit

Permalink
Add completion for import fields in cabal files (#4305)
Browse files Browse the repository at this point in the history
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

Co-authored-by: Michael Peyton Jones <[email protected]>
  • Loading branch information
VeryMilkyJoe and michaelpj authored Jun 16, 2024
1 parent da3d7f2 commit 62892ae
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 72 deletions.
1 change: 1 addition & 0 deletions haskell-language-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ library hls-cabal-plugin
exposed-modules:
Ide.Plugin.Cabal
Ide.Plugin.Cabal.Diagnostics
Ide.Plugin.Cabal.Completion.CabalFields
Ide.Plugin.Cabal.Completion.Completer.FilePath
Ide.Plugin.Cabal.Completion.Completer.Module
Ide.Plugin.Cabal.Completion.Completer.Paths
Expand Down
15 changes: 14 additions & 1 deletion plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Data.Hashable
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.List.NonEmpty as NE
import qualified Data.Maybe as Maybe
import qualified Data.Text.Encoding as Encoding
import Data.Typeable
import Development.IDE as D
Expand All @@ -32,7 +33,8 @@ import qualified Distribution.Parsec.Position as Syntax
import GHC.Generics
import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes
import qualified Ide.Plugin.Cabal.Completion.Completions as Completions
import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..),
import Ide.Plugin.Cabal.Completion.Types (ParseCabalCommonSections (ParseCabalCommonSections),
ParseCabalFields (..),
ParseCabalFile (..))
import qualified Ide.Plugin.Cabal.Completion.Types as Types
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
Expand Down Expand Up @@ -170,6 +172,14 @@ cabalRules recorder plId = do
Right fields ->
pure ([], Just fields)

define (cmapWithPrio LogShake recorder) $ \ParseCabalCommonSections file -> do
fields <- use_ ParseCabalFields file
let commonSections = Maybe.mapMaybe (\case
commonSection@(Syntax.Section (Syntax.Name _ "common") _ _) -> Just commonSection
_ -> Nothing)
fields
pure ([], Just commonSections)

define (cmapWithPrio LogShake recorder) $ \ParseCabalFile file -> do
config <- getPluginConfigAction plId
if not (plcGlobalOn config && plcDiagnosticsOn config)
Expand Down Expand Up @@ -342,6 +352,9 @@ completion recorder ide _ complParams = do
-- The `withStale` option is very important here, since we often call this rule with invalid cabal files.
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast ParseCabalFile $ toNormalizedFilePath fp
pure $ fmap fst mGPD
, getCabalCommonSections = do
mSections <- runIdeAction "cabal-plugin.modulesCompleter.commonsections" (shakeExtras ide) $ useWithStaleFast ParseCabalCommonSections $ toNormalizedFilePath fp
pure $ fmap fst mSections
, cabalPrefixInfo = prefInfo
, stanzaName =
case fst ctx of
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Ide.Plugin.Cabal.Completion.CabalFields (findStanzaForColumn, findFieldSection, getOptionalSectionName, getAnnotation, getFieldName) where

import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Distribution.Fields as Syntax
import qualified Distribution.Parsec.Position as Syntax
import Ide.Plugin.Cabal.Completion.Types

-- ----------------------------------------------------------------
-- Cabal-syntax utilities I don't really want to write myself
-- ----------------------------------------------------------------

-- | Determine the context of a cursor position within a stack of stanza contexts
--
-- If the cursor is indented more than one of the stanzas in the stack
-- the respective stanza is returned if this is never the case, the toplevel stanza
-- in the stack is returned.
findStanzaForColumn :: Int -> NonEmpty (Int, StanzaContext) -> (StanzaContext, FieldContext)
findStanzaForColumn col ctx = case NE.uncons ctx of
((_, stanza), Nothing) -> (stanza, None)
((indentation, stanza), Just res)
| col < indentation -> findStanzaForColumn col res
| otherwise -> (stanza, None)

-- | Determine the field the cursor is currently a part of.
--
-- The result is said field and its starting position
-- or Nothing if the passed list of fields is empty.

-- This only looks at the row of the cursor and not at the cursor's
-- position within the row.
--
-- TODO: we do not handle braces correctly. Add more tests!
findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position)
findFieldSection _cursor [] = Nothing
findFieldSection _cursor [x] =
-- Last field. We decide later, whether we are starting
-- a new section.
Just x
findFieldSection cursor (x:y:ys)
| Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y)
= Just x
| otherwise = findFieldSection cursor (y:ys)
where
cursorLine = Syntax.positionRow cursor

type FieldName = T.Text

getAnnotation :: Syntax.Field ann -> ann
getAnnotation (Syntax.Field (Syntax.Name ann _) _) = ann
getAnnotation (Syntax.Section (Syntax.Name ann _) _ _) = ann

getFieldName :: Syntax.Field ann -> FieldName
getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn
getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn

-- | Returns the name of a section if it has a name.
--
-- This assumes that the given section args belong to named stanza
-- in which case the stanza name is returned.
getOptionalSectionName :: [Syntax.SectionArg ann] -> Maybe T.Text
getOptionalSectionName [] = Nothing
getOptionalSectionName (x:xs) = case x of
Syntax.SecArgName _ name -> Just (T.decodeUtf8 name)
_ -> getOptionalSectionName xs

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Ide.Plugin.Cabal.Completion.Completer.Simple where
Expand All @@ -7,11 +8,14 @@ import Data.Function ((&))
import qualified Data.List as List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Maybe (fromMaybe,
mapMaybe)
import Data.Ord (Down (Down))
import qualified Data.Text as T
import qualified Distribution.Fields as Syntax
import Ide.Logger (Priority (..),
logWith)
import Ide.Plugin.Cabal.Completion.CabalFields
import Ide.Plugin.Cabal.Completion.Completer.Types
import Ide.Plugin.Cabal.Completion.Types (CabalPrefixInfo (..),
Log)
Expand Down Expand Up @@ -41,6 +45,22 @@ constantCompleter completions _ cData = do
range = completionRange prefInfo
pure $ map (mkSimpleCompletionItem range . Fuzzy.original) scored

-- | Completer to be used for import fields.
--
-- TODO: Does not exclude imports, defined after the current cursor position
-- which are not allowed according to the cabal specification
importCompleter :: Completer
importCompleter l cData = do
cabalCommonsM <- getCabalCommonSections cData
case cabalCommonsM of
Just cabalCommons -> do
let commonNames = mapMaybe (\case
Syntax.Section (Syntax.Name _ "common") commonNames _ -> getOptionalSectionName commonNames
_ -> Nothing)
cabalCommons
constantCompleter commonNames l cData
Nothing -> noopCompleter l cData

-- | Completer to be used for the field @name:@ value.
--
-- This is almost always the name of the cabal file. However,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
module Ide.Plugin.Cabal.Completion.Completer.Types where

import Development.IDE as D
import qualified Distribution.Fields as Syntax
import Distribution.PackageDescription (GenericPackageDescription)
import qualified Distribution.Parsec.Position as Syntax
import Ide.Plugin.Cabal.Completion.Types
import Language.LSP.Protocol.Types (CompletionItem)

Expand All @@ -16,9 +18,11 @@ data CompleterData = CompleterData
{ -- | Access to the latest available generic package description for the handled cabal file,
-- relevant for some completion actions which require the file's meta information
-- such as the module completers which require access to source directories
getLatestGPD :: IO (Maybe GenericPackageDescription),
getLatestGPD :: IO (Maybe GenericPackageDescription),
-- | Access to the entries of the handled cabal file as parsed by ParseCabalFields
getCabalCommonSections :: IO (Maybe [Syntax.Field Syntax.Position]),
-- | Prefix info to be used for constructing completion items
cabalPrefixInfo :: CabalPrefixInfo,
cabalPrefixInfo :: CabalPrefixInfo,
-- | The name of the stanza in which the completer is applied
stanzaName :: Maybe StanzaName
stanzaName :: Maybe StanzaName
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Development.IDE as D
import qualified Development.IDE.Plugin.Completions.Types as Ghcide
import qualified Distribution.Fields as Syntax
import qualified Distribution.Parsec.Position as Syntax
import Ide.Plugin.Cabal.Completion.CabalFields
import Ide.Plugin.Cabal.Completion.Completer.Simple
import Ide.Plugin.Cabal.Completion.Completer.Snippet
import Ide.Plugin.Cabal.Completion.Completer.Types (Completer)
Expand Down Expand Up @@ -177,57 +177,3 @@ classifyFieldContext ctx cursor field

cursorColumn = Syntax.positionCol cursor
fieldColumn = Syntax.positionCol (getAnnotation field)

-- ----------------------------------------------------------------
-- Cabal-syntax utilities I don't really want to write myself
-- ----------------------------------------------------------------

-- | Determine the context of a cursor position within a stack of stanza contexts
--
-- If the cursor is indented more than one of the stanzas in the stack
-- the respective stanza is returned if this is never the case, the toplevel stanza
-- in the stack is returned.
findStanzaForColumn :: Int -> NonEmpty (Int, StanzaContext) -> (StanzaContext, FieldContext)
findStanzaForColumn col ctx = case NE.uncons ctx of
((_, stanza), Nothing) -> (stanza, None)
((indentation, stanza), Just res)
| col < indentation -> findStanzaForColumn col res
| otherwise -> (stanza, None)

-- | Determine the field the cursor is currently a part of.
--
-- The result is said field and its starting position
-- or Nothing if the passed list of fields is empty.

-- This only looks at the row of the cursor and not at the cursor's
-- position within the row.
--
-- TODO: we do not handle braces correctly. Add more tests!
findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position)
findFieldSection _cursor [] = Nothing
findFieldSection _cursor [x] =
-- Last field. We decide later, whether we are starting
-- a new section.
Just x
findFieldSection cursor (x:y:ys)
| Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y)
= Just x
| otherwise = findFieldSection cursor (y:ys)
where
cursorLine = Syntax.positionRow cursor

type FieldName = T.Text

getAnnotation :: Syntax.Field ann -> ann
getAnnotation (Syntax.Field (Syntax.Name ann _) _) = ann
getAnnotation (Syntax.Section (Syntax.Name ann _) _ _) = ann

getFieldName :: Syntax.Field ann -> FieldName
getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn
getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn

getOptionalSectionName :: [Syntax.SectionArg ann] -> Maybe T.Text
getOptionalSectionName [] = Nothing
getOptionalSectionName (x:xs) = case x of
Syntax.SecArgName _ name -> Just (T.decodeUtf8 name)
_ -> getOptionalSectionName xs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ flagFields =
libExecTestBenchCommons :: Map KeyWordName Completer
libExecTestBenchCommons =
Map.fromList
[ ("build-depends:", noopCompleter),
[ ("import:", importCompleter),
("build-depends:", noopCompleter),
("hs-source-dirs:", directoryCompleter),
("default-extensions:", noopCompleter),
("other-extensions:", noopCompleter),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ instance Hashable ParseCabalFields

instance NFData ParseCabalFields

type instance RuleResult ParseCabalCommonSections = [Syntax.Field Syntax.Position]

data ParseCabalCommonSections = ParseCabalCommonSections
deriving (Eq, Show, Typeable, Generic)

instance Hashable ParseCabalCommonSections

instance NFData ParseCabalCommonSections

-- | The context a cursor can be in within a cabal file.
--
-- We can be in stanzas or the top level,
Expand Down
Loading

0 comments on commit 62892ae

Please sign in to comment.