@@ -29,11 +29,20 @@ import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
2929import qualified Ide.Plugin.Cabal.Parse as Parse
3030import Ide.Plugin.Config (Config )
3131import Ide.Types
32- import Language.LSP.Server ( LspM )
32+ import qualified Language.LSP.Server as LSP
3333import Language.LSP.Types
3434import qualified Language.LSP.Types as LSP
3535import qualified Language.LSP.VFS as VFS
36-
36+ import qualified Data.Text as T
37+ import qualified Language.LSP.Types.Lens as JL
38+ import qualified Language.LSP.Types as J
39+ import Distribution.Compat.Lens ((^.) )
40+ import qualified Text.Fuzzy.Parallel as Fuzzy
41+ import Data.Map (Map )
42+ import qualified Data.Map as Map
43+ import Language.LSP.VFS (VirtualFile )
44+ import qualified Data.Text.Utf16.Rope as Rope
45+ import qualified Data.List as List
3746data Log
3847 = LogModificationTime NormalizedFilePath (Maybe FileVersion )
3948 | LogDiagnostics NormalizedFilePath [FileDiagnostic ]
@@ -63,7 +72,8 @@ instance Pretty Log where
6372descriptor :: Recorder (WithPriority Log ) -> PluginId -> PluginDescriptor IdeState
6473descriptor recorder plId = (defaultCabalPluginDescriptor plId)
6574 { pluginRules = cabalRules recorder
66- , pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
75+ , pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
76+ <> mkPluginHandler J. STextDocumentCompletion completion
6777 , pluginNotificationHandlers = mconcat
6878 [ mkPluginNotificationHandler LSP. STextDocumentDidOpen $
6979 \ ide vfs _ (DidOpenTextDocumentParams TextDocumentItem {_uri,_version}) -> liftIO $ do
@@ -149,6 +159,208 @@ licenseSuggestCodeAction
149159 :: IdeState
150160 -> PluginId
151161 -> CodeActionParams
152- -> LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
162+ -> LSP. LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
153163licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext {_diagnostics= List diags}) =
154164 pure $ Right $ List $ mapMaybe (fmap InR . LicenseSuggest. licenseErrorAction uri) diags
165+
166+ -- ----------------------------------------------------------------
167+ -- Completion
168+ -- ----------------------------------------------------------------
169+ completion :: PluginMethodHandler IdeState 'J.TextDocumentCompletion
170+ completion _ide _ complParams = do
171+ let (J. TextDocumentIdentifier uri) = complParams ^. JL. textDocument
172+ position = complParams ^. JL. position
173+ contents <- LSP. getVirtualFile $ toNormalizedUri uri
174+ fmap (Right . J. InL ) $ case (contents, uriToFilePath' uri) of
175+ (Just cnts, Just _path) -> do
176+ pref <- VFS. getCompletionPrefix position cnts
177+ return $ result pref cnts
178+ _ -> return $ J. List []
179+ where
180+ result :: Maybe VFS. PosPrefixInfo -> VirtualFile -> J. List CompletionItem
181+ result Nothing _ = J. List []
182+ result (Just pfix) cnts
183+ | (VFS. cursorPos pfix) ^. JL. line == 0 = J. List [buildCompletion cabalVersionKeyword]
184+ | Stanza s <- findCurrentLevel (getPreviousLines pfix cnts) =
185+ case (Map. lookup s stanzaKeywordMap) of
186+ Nothing ->
187+ J. List $
188+ makeCompletionItems pfix topLevelKeywords
189+ Just l -> J. List $ (makeCompletionItems pfix l) ++ (makeCompletionItems pfix $ Map. keys stanzaKeywordMap)
190+ | otherwise =
191+ J. List $
192+ makeCompletionItems pfix topLevelKeywords
193+ where
194+ topLevelKeywords = cabalKeywords ++ Map. keys stanzaKeywordMap
195+
196+ -- | Takes info about the current cursor position and a set of possible keywords
197+ -- and creates completion suggestions that fit the current input from the given list
198+ makeCompletionItems :: VFS. PosPrefixInfo -> [T. Text ] -> [CompletionItem ]
199+ makeCompletionItems pfix l =
200+ map
201+ (buildCompletion . Fuzzy. original)
202+ (Fuzzy. simpleFilter 1000 10 (VFS. prefixText pfix) l)
203+
204+ -- | Parse the given set of lines (starting before current cursor position
205+ -- up to the start of the file) to find the nearest stanza declaration,
206+ -- if none is found we are in the top level
207+ findCurrentLevel :: [T. Text ] -> Context
208+ findCurrentLevel [] = TopLevel
209+ findCurrentLevel (cur : xs)
210+ | Just s <- stanza = Stanza s
211+ | otherwise = findCurrentLevel xs
212+ where
213+ stanza = List. find (`T.isPrefixOf` cur) (Map. keys stanzaKeywordMap)
214+
215+ -- | Get all lines before the given cursor position in the given file
216+ -- and reverse them since we want to traverse starting from our current position
217+ getPreviousLines :: VFS. PosPrefixInfo -> VirtualFile -> [T. Text ]
218+ getPreviousLines pos cont = reverse $ take (fromIntegral currentLine) allLines
219+ where
220+ allLines = Rope. lines $ cont ^. VFS. file_text
221+ currentLine = (VFS. cursorPos pos) ^. JL. line
222+
223+
224+ data Context
225+ = TopLevel
226+ -- ^ top level context in a cabal file such as 'author'
227+ | Stanza T. Text
228+ -- ^ nested context in a cabal file, such as 'library', which has nested keywords, specific to the stanza
229+ deriving (Eq )
230+
231+ -- | Keyword for cabal version required to be the top line in a cabal file
232+ cabalVersionKeyword :: T. Text
233+ cabalVersionKeyword = " cabal-version:"
234+
235+ -- | Top level keywords of a cabal file
236+ cabalKeywords :: [T. Text ]
237+ cabalKeywords =
238+ [
239+ " name:" ,
240+ " version:" ,
241+ " build-type:" ,
242+ " license:" ,
243+ " license-file:" ,
244+ " license-files:" ,
245+ " copyright:" ,
246+ " author:" ,
247+ " maintainer:" ,
248+ " stability:" ,
249+ " homepage:" ,
250+ " bug-reports:" ,
251+ " package-url:" ,
252+ " synopsis:" ,
253+ " description:" ,
254+ " category:" ,
255+ " tested-with:" ,
256+ " data-files:" ,
257+ " data-dir:" ,
258+ " data-dir:" ,
259+ " extra-doc-files:" ,
260+ " extra-tmp-files:"
261+ ]
262+
263+ -- | Map, containing all stanzas in a cabal file as keys and lists of their possible nested keywords as values
264+ stanzaKeywordMap :: Map T. Text [T. Text ]
265+ stanzaKeywordMap = Map. fromList [(" library" , [
266+ " exposed-modules:" ,
267+ " virtual-modules:" ,
268+ " exposed:" ,
269+ " visibility:" ,
270+ " reexported-modules:" ,
271+ " signatures:"
272+ ])]
273+
274+
275+ -- TODO move out toplevel commands i.e. test-suite
276+ -- cabalTestKeywords :: [T.Text]
277+ -- cabalTestKeywords =
278+ -- [
279+ -- "test-suite",
280+ -- "type:",
281+ -- "main-is:",
282+ -- "test-module:",
283+ -- "benchmark",
284+ -- "main-is:",
285+ -- "foreign-library",
286+ -- "type:",
287+ -- "options:",
288+ -- "mod-def-file:",
289+ -- "lib-version-info:",
290+ -- "lib-version-linux:",
291+ -- "build-depends:",
292+ -- "other-modules:",
293+ -- "hs-source-dir:",
294+ -- "hs-source-dirs:",
295+ -- "default-extensions:",
296+ -- "other-extensions:",
297+ -- "default-language:",
298+ -- "other-languages:",
299+ -- "extensions:",
300+ -- "build-tool-depends:",
301+ -- "build-tools:",
302+ -- "buildable:",
303+ -- "ghc-options:",
304+ -- "ghc-prof-options:",
305+ -- "ghc-shared-options:",
306+ -- "ghcjs-options:",
307+ -- "ghcjs-prof-options:",
308+ -- "ghcjs-shared-options:",
309+ -- "includes:",
310+ -- "install-includes:",
311+ -- ("include-dirs:", "directory list"),
312+ -- ("c-sources:", "filename list"),
313+ -- ("cxx-sources:", "filename list"),
314+ -- ("asm-sources:", "filename list"),
315+ -- ("cmm-sources:", "filename list"),
316+ -- ("js-sources:", "filename list"),
317+ -- ("extra-libraries:", "token list"),
318+ -- ("extra-libraries-static:", "token list"),
319+ -- ("extra-ghci-libraries:", "token list"),
320+ -- ("extra-bundled-libraries:", "token list"),
321+ -- ("extra-lib-dirs:", "directory list")
322+ -- ("extra-lib-dirs-static:", "directory list"),
323+ -- ("extra-library-flavours:", "notsure"),
324+ -- ("extra-dynamic-library-flavours:", "notsure"),
325+ -- ("cc-options:", "token list"),
326+ -- ("cpp-options:", "token list"),
327+ -- ("cxx-options:", "token list"),
328+ -- ("cmm-options:", "token list"),
329+ -- ("asm-options:", "token list"),
330+ -- ("ld-options:", "token list"),
331+ -- ("hsc2hs-options:", "token list"),
332+ -- ("pkgconfig-depends:", "package list"),
333+ -- ("frameworks:", "token list"),
334+ -- ("extra-framework-dirs:", "directory list"),
335+ -- ("mixins:", "mixin list")
336+ -- ]
337+
338+ -- cabalFlagKeywords :: [(T.Text, T.Text)]
339+ -- cabalFlagKeywords =
340+ -- [
341+ -- ("flag", "name"),
342+ -- ("description:", "freeform"),
343+ -- ("default:", "boolean"),
344+ -- ("manual:", "boolean")
345+ -- ]
346+
347+ -- cabalStanzaKeywords :: [(T.Text, T.Text)]
348+ -- cabalStanzaKeywords =
349+ -- [
350+ -- ("common", "name"),
351+ -- ("import:", "token-list")
352+ -- ]
353+
354+ -- cabalSourceRepoKeywords :: [(T.Text, T.Text)]
355+ -- cabalSourceRepoKeywords =
356+ -- [
357+ -- ("source-repository", ""),
358+ -- ("type:", "token"),
359+ -- ("location:", "URL")
360+ -- ]
361+
362+ buildCompletion :: T. Text -> J. CompletionItem
363+ buildCompletion label =
364+ J. CompletionItem label (Just J. CiKeyword ) Nothing Nothing
365+ Nothing Nothing Nothing Nothing Nothing Nothing Nothing
366+ Nothing Nothing Nothing Nothing Nothing Nothing
0 commit comments