@@ -10,8 +10,8 @@ import Data.HashMap.Strict (HashMap)
1010import qualified Data.HashMap.Strict as HM
1111import qualified Data.HashSet as HS
1212import Data.List (uncons )
13- import Data.Maybe (catMaybes , listToMaybe ,
14- mapMaybe )
13+ import Data.Maybe (catMaybes , fromMaybe ,
14+ listToMaybe , mapMaybe )
1515import Data.Text (Text , intercalate )
1616import qualified Data.Text as T
1717import qualified Data.Text.Utf16.Rope.Mixed as Rope
@@ -25,8 +25,8 @@ import GHC.Generics (Generic)
2525import Ide.Plugin.Error (PluginError (.. ))
2626import Ide.Types
2727import qualified Language.LSP.Protocol.Lens as L
28- import Language.LSP.Protocol.Message (Method (Method_TextDocumentDefinition , Method_TextDocumentReferences ),
29- SMethod (SMethod_TextDocumentDefinition , SMethod_TextDocumentReferences ))
28+ import Language.LSP.Protocol.Message (Method (Method_TextDocumentCompletion , Method_TextDocumentDefinition , Method_TextDocumentHover , Method_TextDocumentReferences ),
29+ SMethod (SMethod_TextDocumentCompletion , SMethod_TextDocumentDefinition , SMethod_TextDocumentHover , SMethod_TextDocumentReferences ))
3030import Language.LSP.Protocol.Types
3131import Text.Regex.TDFA (Regex , caseSensitive ,
3232 defaultCompOpt ,
@@ -80,6 +80,8 @@ descriptor recorder plId = (defaultPluginDescriptor plId "Provides goto definiti
8080 , Ide.Types. pluginHandlers =
8181 mkPluginHandler SMethod_TextDocumentDefinition jumpToNote
8282 <> mkPluginHandler SMethod_TextDocumentReferences listReferences
83+ <> mkPluginHandler SMethod_TextDocumentHover hoverNote
84+ <> mkPluginHandler SMethod_TextDocumentCompletion autocomplete
8385 }
8486
8587findNotesRules :: Recorder (WithPriority Log ) -> Rules ()
@@ -190,8 +192,199 @@ findNotesInFile file recorder = do
190192
191193noteRefRegex , noteRegex :: Regex
192194(noteRefRegex, noteRegex) =
193- ( mkReg (" note \\ [(.+)\\ ]" :: String )
194- , mkReg (" note \\ [([[:print:]]+)\\ ][[:blank:]]*\r ?\n [[:blank:]]*(--)?[[:blank:]]*~~~" :: String )
195+ ( mkReg (" note[[:blank:]]? \\ [(.+)\\ ]" :: String )
196+ , mkReg (" note[[:blank:]]? \\ [([[:print:]]+)\\ ][[:blank:]]*\r ?\n [[:blank:]]*(--)?[[:blank:]]*~~~" :: String )
195197 )
196198 where
197199 mkReg = makeRegexOpts (defaultCompOpt { caseSensitive = False }) defaultExecOpt
200+
201+ -- | Find the precise range of `note[...]` or `note [...]` on a line.
202+ findNoteRange
203+ :: Text -- ^ Full line text
204+ -> Text -- ^ Note title
205+ -> UInt -- ^ Line number
206+ -> Maybe Range
207+ findNoteRange line _note lineNo =
208+ case matchAllText noteRefRegex line of
209+ [] -> Nothing
210+ (arr: _) ->
211+ case arr A. ! 0 of
212+ (_, (start, len)) ->
213+ let startCol = fromIntegral start
214+ endCol = startCol + fromIntegral len
215+ in Just $
216+ Range
217+ (Position lineNo startCol)
218+ (Position lineNo endCol)
219+
220+ -- Given the path and position of a Note Declaration, finds Content in it.
221+ -- ignores ~ as a seprator
222+ extractNoteContent
223+ :: NormalizedFilePath
224+ -> Position
225+ -> IO (Maybe Text )
226+ extractNoteContent nfp (Position startLine _) = do
227+ fileText <- readFileUtf8 $ fromNormalizedFilePath nfp
228+
229+ let allLines = T. lines fileText
230+ afterDecl = drop (fromIntegral startLine + 1 ) allLines
231+ afterSeparator =
232+ case dropWhile (not . isSeparatorLine) afterDecl of
233+ [] -> []
234+ (_: xs) -> xs
235+
236+ bodyLines = takeWhile (not . isStopLine) afterSeparator
237+
238+ if null afterSeparator
239+ then pure Nothing
240+ else pure $ Just (T. unlines (map stripCommentPrefix bodyLines))
241+
242+ where
243+ isSeparatorLine :: Text -> Bool
244+ isSeparatorLine t = " ~~~" `T.isInfixOf` t
245+
246+ isNoteHeader :: Text -> Bool
247+ isNoteHeader t =
248+ let stripped = T. toLower (T. stripStart (stripCommentPrefix t))
249+ in " note [" `T.isPrefixOf` stripped
250+
251+ isEndMarker :: Text -> Bool
252+ isEndMarker t = " -}" `T.isInfixOf` t
253+
254+ isStopLine :: Text -> Bool
255+ isStopLine t = isSeparatorLine t || isNoteHeader t || isEndMarker t
256+
257+ stripCommentPrefix :: Text -> Text
258+ stripCommentPrefix t =
259+ let s = T. stripStart t
260+ in if " --" `T.isPrefixOf` s
261+ then T. stripStart (T. drop 2 s)
262+ else s
263+
264+ normalizeNewlines :: Text -> Text
265+ normalizeNewlines = T. replace " \r\n " " \n "
266+
267+ -- on hovering Note References, shows corresponding Declaration if it Exists
268+ -- ignores Note Declaration
269+ hoverNote :: PluginMethodHandler IdeState Method_TextDocumentHover
270+ hoverNote state _ params
271+ | Just nfp <- uriToNormalizedFilePath uriOrig
272+ = do
273+ let pos@ (Position line _) = params ^. L. position
274+ noteOpt <- getNote nfp state pos
275+ case noteOpt of
276+ Nothing -> pure (InR Null )
277+
278+ Just note -> do
279+ mbRope <- liftIO $ runAction " notes.hoverLine" state (getFileContents nfp)
280+
281+ -- compute precise hover range for highlighting corresponding Note Reference on Hover
282+ let lineText =
283+ case mbRope of
284+ Nothing -> " "
285+ Just rope -> fromMaybe " " $ listToMaybe $ drop (fromIntegral line) $ Rope. lines rope
286+
287+ mbRange = findNoteRange lineText note line
288+
289+ notes <- runActionE " notes.hover" state $ useE MkGetNotes nfp
290+ case HM. lookup note notes of
291+ Nothing -> pure $ InL $ Hover (InL $ MarkupContent MarkupKind_Markdown " _No declaration available_" ) mbRange
292+
293+ Just (defFile, defPos) ->
294+ if nfp == defFile && pos ^. L. line == defPos ^. L. line
295+ then pure (InR Null )
296+ else do
297+ mbContent <- liftIO $ extractNoteContent defFile defPos
298+
299+ let contents =
300+ case mbContent of
301+ Nothing -> " _No declaration available_"
302+ Just body -> T. unlines [note, " " , body]
303+ normalizedContents = normalizeNewlines contents
304+
305+ pure $ InL $ Hover (InL $ MarkupContent MarkupKind_Markdown normalizedContents) mbRange
306+ where
307+ uriOrig = toNormalizedUri $ params ^. (L. textDocument . L. uri)
308+
309+ hoverNote _ _ _ = pure (InR Null )
310+
311+ -- Gives an autocomplete suggestion when 'note' prefix is detected
312+ autocomplete :: PluginMethodHandler IdeState Method_TextDocumentCompletion
313+ autocomplete state _ params = do
314+ let uri = params ^. (L. textDocument . L. uri)
315+ pos = params ^. L. position
316+ nuri = toNormalizedUri uri
317+
318+ contents <-
319+ liftIO $
320+ runAction " Notes.GetUriContents" state $
321+ getUriContents nuri
322+
323+ fmap InL $
324+ case contents of
325+ Nothing -> pure []
326+
327+ Just rope -> do
328+ let linePrefix = T. toLower $ T. stripEnd $ getLinePrefix rope pos
329+
330+ -- Suggest NOTE DECLARATION snippit if "note" prefix detected
331+ if T. strip linePrefix == " note"
332+ then
333+ pure [CompletionItem " Note" Nothing (Just CompletionItemKind_Keyword ) Nothing
334+ (Just " Note Declaration" ) Nothing Nothing Nothing Nothing
335+ Nothing (Just noteSnippet) (Just InsertTextFormat_Snippet ) Nothing
336+ Nothing Nothing Nothing Nothing Nothing Nothing
337+ ]
338+
339+ -- Suggest list of all NOTE DECLARATION if "note [" infix detected
340+ else if " note[" `T.isInfixOf` linePrefix || " note [" `T.isInfixOf` linePrefix
341+ then
342+ case uriToNormalizedFilePath nuri of
343+ Nothing -> pure []
344+
345+ Just nfp -> do
346+ let typed =
347+ case T. breakOnEnd " [" linePrefix of
348+ (_, " " ) -> " "
349+ (_, rest)-> T. strip rest
350+
351+ notesMap <-
352+ runActionE " notes.completion.notes" state $
353+ useE MkGetNotes nfp
354+
355+ let allNotes = HM. keys notesMap
356+ matches =
357+ filter
358+ (\ n -> T. toLower typed `T.isPrefixOf` T. toLower n)
359+ allNotes
360+
361+ finalNotes =
362+ if null matches then allNotes else matches
363+ pure $
364+ map
365+ (\ n ->
366+ CompletionItem n Nothing (Just CompletionItemKind_Reference ) Nothing (Just " Note reference" )
367+ Nothing Nothing (Just True ) (Just " 0" ) (Just n)
368+ Nothing Nothing Nothing Nothing Nothing
369+ Nothing Nothing Nothing Nothing
370+ )
371+ finalNotes
372+ else
373+ pure []
374+
375+ noteSnippet :: Text
376+ noteSnippet =
377+ T. unlines
378+ [ " {- Note [${1:Declaration title}]"
379+ , " ~~~"
380+ , " ${2:Content}"
381+ , " -}"
382+ ]
383+
384+ getLinePrefix :: Rope. Rope -> Position -> Text
385+ getLinePrefix rope (Position line col) =
386+ case Rope. splitAtLine (fromIntegral line) rope of
387+ (_, rest) ->
388+ case Rope. lines rest of
389+ (l: _) -> T. take (fromIntegral col) l
390+ _ -> " "
0 commit comments