haskell-language-server-2.10.0.0: plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs
module Ide.Plugin.Cabal.Completion.CabalFields
( findStanzaForColumn
, getModulesNames
, getFieldLSPRange
, findFieldSection
, findTextWord
, findFieldLine
, getOptionalSectionName
, getAnnotation
, getFieldName
, onelineSectionArgs
, getFieldEndPosition
, getSectionArgEndPosition
, getNameEndPosition
, getFieldLineEndPosition
)
where
import qualified Data.ByteString as BS
import Data.List (find)
import Data.List.Extra (groupSort)
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 Data.Tuple (swap)
import qualified Distribution.Fields as Syntax
import qualified Distribution.Parsec.Position as Syntax
import Ide.Plugin.Cabal.Completion.Types
import qualified Language.LSP.Protocol.Types as LSP
-- ----------------------------------------------------------------
-- 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
-- | Determine the field line the cursor is currently a part of.
--
-- The result is said field line and its starting position
-- or Nothing if the passed list of fields is empty.
--
-- This function assumes that elements in a field's @FieldLine@ list
-- do not share the same row.
findFieldLine :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.FieldLine Syntax.Position)
findFieldLine _cursor [] = Nothing
findFieldLine cursor fields =
case findFieldSection cursor fields of
Nothing -> Nothing
Just (Syntax.Field _ fieldLines) -> find filterLineFields fieldLines
Just (Syntax.Section _ _ fields) -> findFieldLine cursor fields
where
cursorLine = Syntax.positionRow cursor
-- In contrast to `Field` or `Section`, `FieldLine` must have the exact
-- same line position as the cursor.
filterLineFields (Syntax.FieldLine pos _) = Syntax.positionRow pos == cursorLine
-- | Determine the exact word at the current cursor position.
--
-- The result is said word or Nothing if the passed list is empty
-- or the cursor position is not next to, or on a word.
-- For this function, a word is a sequence of consecutive characters
-- that are not a space or column.
--
-- This function currently only considers words inside of a @FieldLine@.
findTextWord :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe T.Text
findTextWord _cursor [] = Nothing
findTextWord cursor fields =
case findFieldLine cursor fields of
Nothing -> Nothing
Just (Syntax.FieldLine pos byteString) ->
let decodedText = T.decodeUtf8 byteString
lineFieldCol = Syntax.positionCol pos
lineFieldLen = T.length decodedText
offset = cursorCol - lineFieldCol in
-- Range check if cursor is inside or or next to found line.
-- The latter comparison includes the length of the line as offset,
-- which is done to also include cursors that are at the end of a line.
-- e.g. "foo,bar|"
-- ^
-- cursor
--
-- Having an offset which is outside of the line is possible because of `splitAt`.
if offset >= 0 && lineFieldLen >= offset
then
let (lhs, rhs) = T.splitAt offset decodedText
strippedLhs = T.takeWhileEnd isAllowedChar lhs
strippedRhs = T.takeWhile isAllowedChar rhs
resultText = T.concat [strippedLhs, strippedRhs] in
-- It could be possible that the cursor was in-between separators, in this
-- case the resulting text would be empty, which should result in `Nothing`.
-- e.g. " foo ,| bar"
-- ^
-- cursor
if not $ T.null resultText then Just resultText else Nothing
else
Nothing
where
cursorCol = Syntax.positionCol cursor
separators = [',', ' ']
isAllowedChar = (`notElem` separators)
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
getFieldLineName :: Syntax.FieldLine ann -> FieldName
getFieldLineName (Syntax.FieldLine _ 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
type BuildTargetName = T.Text
type ModuleName = T.Text
-- | Given a cabal AST returns pairs of all respective target names
-- and the module name bound to them. If a target is a main library gives
-- @Nothing@, otherwise @Just target-name@
--
-- Examples of input cabal files and the outputs:
--
-- * Target is a main library module:
--
-- > library
-- > exposed-modules:
-- > MyLib
--
-- * @getModulesNames@ output:
--
-- > [([Nothing], "MyLib")]
--
-- * Same module names in different targets:
--
-- > test-suite first-target
-- > other-modules:
-- > Config
-- > test-suite second-target
-- > other-modules:
-- > Config
--
-- * @getModulesNames@ output:
--
-- > [([Just "first-target", Just "second-target"], "Config")]
getModulesNames :: [Syntax.Field any] -> [([Maybe BuildTargetName], ModuleName)]
getModulesNames fields = map swap $ groupSort rawModuleTargetPairs
where
rawModuleTargetPairs = concatMap getSectionModuleNames sections
sections = getSectionsWithModules fields
getSectionModuleNames :: Syntax.Field any -> [(ModuleName, Maybe BuildTargetName)]
getSectionModuleNames (Syntax.Section _ secArgs fields) = map (, getArgsName secArgs) $ concatMap getFieldModuleNames fields
getSectionModuleNames _ = []
getArgsName [Syntax.SecArgName _ name] = Just $ T.decodeUtf8 name
getArgsName _ = Nothing -- Can be only a main library, that has no name
-- since it's impossible to have multiple names for a build target
getFieldModuleNames field@(Syntax.Field _ modules) = if getFieldName field == T.pack "exposed-modules" ||
getFieldName field == T.pack "other-modules"
then map getFieldLineName modules
else []
getFieldModuleNames _ = []
-- | Trims a given cabal AST leaving only targets and their
-- @exposed-modules@ and @other-modules@ sections.
--
-- For example:
--
-- * Given a cabal file like this:
--
-- > library
-- > import: extra
-- > hs-source-dirs: source/directory
-- > ...
-- > exposed-modules:
-- > Important.Exposed.Module
-- > other-modules:
-- > Important.Other.Module
-- >
-- > test-suite tests
-- > type: type
-- > build-tool-depends: tool
-- > other-modules:
-- > Important.Other.Module
--
-- * @getSectionsWithModules@ gives output:
--
-- > library
-- > exposed-modules:
-- > Important.Exposed.Module
-- > other-modules:
-- > Important.Other.Module
-- > test-suite tests
-- > other-modules:
-- > Important.Other.Module
getSectionsWithModules :: [Syntax.Field any] -> [Syntax.Field any]
getSectionsWithModules fields = concatMap go fields
where
go :: Syntax.Field any -> [Syntax.Field any]
go (Syntax.Field _ _) = []
go section@(Syntax.Section _ _ fields) = concatMap onlySectionsWithModules (section:fields)
onlySectionsWithModules :: Syntax.Field any -> [Syntax.Field any]
onlySectionsWithModules (Syntax.Field _ _) = []
onlySectionsWithModules (Syntax.Section name secArgs fields)
| (not . null) newFields = [Syntax.Section name secArgs newFields]
| otherwise = []
where newFields = filter subfieldHasModule fields
subfieldHasModule :: Syntax.Field any -> Bool
subfieldHasModule field@(Syntax.Field _ _) = getFieldName field == T.pack "exposed-modules" ||
getFieldName field == T.pack "other-modules"
subfieldHasModule (Syntax.Section _ _ _) = False
-- | Makes a single text line out of multiple
-- @SectionArg@s. Allows to display conditions,
-- flags, etc in one line, which is easier to read.
--
-- For example, @flag@ @(@ @pedantic@ @)@ will be joined in
-- one line, instead of four @SectionArg@s separately.
onelineSectionArgs :: [Syntax.SectionArg ann] -> T.Text
onelineSectionArgs sectionArgs = joinedName
where
joinedName = T.unwords $ map getName sectionArgs
getName :: Syntax.SectionArg ann -> T.Text
getName (Syntax.SecArgName _ identifier) = T.decodeUtf8 identifier
getName (Syntax.SecArgStr _ quotedString) = T.decodeUtf8 quotedString
getName (Syntax.SecArgOther _ string) = T.decodeUtf8 string
-- | Returns the end position of a provided field
getFieldEndPosition :: Syntax.Field Syntax.Position -> Syntax.Position
getFieldEndPosition (Syntax.Field name []) = getNameEndPosition name
getFieldEndPosition (Syntax.Field _ (x:xs)) = getFieldLineEndPosition $ NE.last (x NE.:| xs)
getFieldEndPosition (Syntax.Section name [] []) = getNameEndPosition name
getFieldEndPosition (Syntax.Section _ (x:xs) []) = getSectionArgEndPosition $ NE.last (x NE.:| xs)
getFieldEndPosition (Syntax.Section _ _ (x:xs)) = getFieldEndPosition $ NE.last (x NE.:| xs)
-- | Returns the end position of a provided section arg
getSectionArgEndPosition :: Syntax.SectionArg Syntax.Position -> Syntax.Position
getSectionArgEndPosition (Syntax.SecArgName (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString)
getSectionArgEndPosition (Syntax.SecArgStr (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString)
getSectionArgEndPosition (Syntax.SecArgOther (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString)
-- | Returns the end position of a provided name
getNameEndPosition :: Syntax.Name Syntax.Position -> Syntax.Position
getNameEndPosition (Syntax.Name (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString)
-- | Returns the end position of a provided field line
getFieldLineEndPosition :: Syntax.FieldLine Syntax.Position -> Syntax.Position
getFieldLineEndPosition (Syntax.FieldLine (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString)
-- | Returns an LSP compatible range for a provided field
getFieldLSPRange :: Syntax.Field Syntax.Position -> LSP.Range
getFieldLSPRange field = LSP.Range startLSPPos endLSPPos
where
startLSPPos = cabalPositionToLSPPosition $ getAnnotation field
endLSPPos = cabalPositionToLSPPosition $ getFieldEndPosition field