-
-
Notifications
You must be signed in to change notification settings - Fork 389
/
Copy pathCabalFields.hs
311 lines (284 loc) · 13.3 KB
/
CabalFields.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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.
findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position)
findFieldSection _cursor [] = Nothing
findFieldSection _cursor [x] = Just x
findFieldSection cursor (x:y:ys)
| Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y)
= Just x
| Syntax.positionRow (getAnnotation x) == cursorLine && Syntax.positionRow (getAnnotation y) == cursorLine
= case (x, y) of
(Syntax.Field _ fieldLines, Syntax.Field _ nextFieldLines) ->
-- Handle multi-line fields with braces
if any (isBraceField . Syntax.fieldLineName) fieldLines
then Just x
else findFieldSection cursor (y:ys)
_ -> findFieldSection cursor (y:ys)
| otherwise = findFieldSection cursor (y:ys)
where
cursorLine = Syntax.positionRow cursor
isBraceField name = name == "extra-libraries:" || name == "extra-frameworks:" || name == "extra-lib-dirs:"
-- | 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
-- | Helper function to check if a field line contains a brace
isBraceField :: T.Text -> Bool
isBraceField name = name `elem` ["extra-libraries:", "extra-frameworks:", "extra-lib-dirs:", "extra-framework-dirs:"]