1
1
module Ide.Plugin.Notes (descriptor , Log ) where
2
2
3
3
import Control.Lens ((^.) )
4
- import Control.Monad.Except (throwError )
4
+ import Control.Monad.Except (ExceptT , MonadError ,
5
+ throwError )
5
6
import Control.Monad.IO.Class (liftIO )
6
7
import qualified Data.Array as A
8
+ import Data.Foldable (foldl' )
7
9
import Data.HashMap.Strict (HashMap )
8
10
import qualified Data.HashMap.Strict as HM
9
11
import qualified Data.HashSet as HS
12
+ import Data.List (uncons )
10
13
import Data.Maybe (catMaybes , listToMaybe ,
11
14
mapMaybe )
12
15
import Data.Text (Text , intercalate )
13
16
import qualified Data.Text as T
14
17
import qualified Data.Text.Utf16.Rope.Mixed as Rope
18
+ import Data.Traversable (for )
15
19
import Development.IDE hiding (line )
16
20
import Development.IDE.Core.PluginUtils (runActionE , useE )
17
21
import Development.IDE.Core.Shake (toKnownFiles )
@@ -21,8 +25,8 @@ import GHC.Generics (Generic)
21
25
import Ide.Plugin.Error (PluginError (.. ))
22
26
import Ide.Types
23
27
import qualified Language.LSP.Protocol.Lens as L
24
- import Language.LSP.Protocol.Message (Method (Method_TextDocumentDefinition ),
25
- SMethod (SMethod_TextDocumentDefinition ))
28
+ import Language.LSP.Protocol.Message (Method (Method_TextDocumentDefinition , Method_TextDocumentReferences ),
29
+ SMethod (SMethod_TextDocumentDefinition , SMethod_TextDocumentReferences ))
26
30
import Language.LSP.Protocol.Types
27
31
import Text.Regex.TDFA (Regex , caseSensitive ,
28
32
defaultCompOpt ,
@@ -31,25 +35,39 @@ import Text.Regex.TDFA (Regex, caseSensitive,
31
35
32
36
data Log
33
37
= LogShake Shake. Log
34
- | LogNotesFound NormalizedFilePath [(Text , Position )]
38
+ | LogNotesFound NormalizedFilePath [(Text , [Position ])]
39
+ | LogNoteReferencesFound NormalizedFilePath [(Text , [Position ])]
35
40
deriving Show
36
41
37
42
data GetNotesInFile = MkGetNotesInFile
38
43
deriving (Show , Generic , Eq , Ord )
39
44
deriving anyclass (Hashable , NFData )
40
- type instance RuleResult GetNotesInFile = HM. HashMap Text Position
45
+ -- The GetNotesInFile action scans the source file and extracts a map of note
46
+ -- definitions (note name -> position) and a map of note references
47
+ -- (note name -> [position]).
48
+ type instance RuleResult GetNotesInFile = (HM. HashMap Text Position , HM. HashMap Text [Position ])
41
49
42
50
data GetNotes = MkGetNotes
43
51
deriving (Show , Generic , Eq , Ord )
44
52
deriving anyclass (Hashable , NFData )
53
+ -- GetNotes collects all note definition across all files in the
54
+ -- project. It returns a map from note name to pair of (filepath, position).
45
55
type instance RuleResult GetNotes = HashMap Text (NormalizedFilePath , Position )
46
56
57
+ data GetNoteReferences = MkGetNoteReferences
58
+ deriving (Show , Generic , Eq , Ord )
59
+ deriving anyclass (Hashable , NFData )
60
+ -- GetNoteReferences collects all note references across all files in the
61
+ -- project. It returns a map from note name to list of (filepath, position).
62
+ type instance RuleResult GetNoteReferences = HashMap Text [(NormalizedFilePath , Position )]
63
+
47
64
instance Pretty Log where
48
65
pretty = \ case
49
- LogShake l -> pretty l
50
- LogNotesFound file notes ->
51
- " Found notes in " <> pretty (show file) <> " : ["
52
- <> pretty (intercalate " , " (fmap (\ (s, p) -> " \" " <> s <> " \" at " <> T. pack (show p)) notes)) <> " ]"
66
+ LogShake l -> pretty l
67
+ LogNoteReferencesFound file refs -> " Found note references in " <> prettyNotes file refs
68
+ LogNotesFound file notes -> " Found notes in " <> prettyNotes file notes
69
+ where prettyNotes file hm = pretty (show file) <> " : ["
70
+ <> pretty (intercalate " , " (fmap (\ (s, p) -> " \" " <> s <> " \" at " <> intercalate " , " (map (T. pack . show ) p)) hm)) <> " ]"
53
71
54
72
{-
55
73
The first time the user requests a jump-to-definition on a note reference, the
@@ -59,7 +77,9 @@ title is then saved in the HLS database to be retrieved for all future requests.
59
77
descriptor :: Recorder (WithPriority Log ) -> PluginId -> PluginDescriptor IdeState
60
78
descriptor recorder plId = (defaultPluginDescriptor plId " Provides goto definition support for GHC-style notes" )
61
79
{ Ide.Types. pluginRules = findNotesRules recorder
62
- , Ide.Types. pluginHandlers = mkPluginHandler SMethod_TextDocumentDefinition jumpToNote
80
+ , Ide.Types. pluginHandlers =
81
+ mkPluginHandler SMethod_TextDocumentDefinition jumpToNote
82
+ <> mkPluginHandler SMethod_TextDocumentReferences listReferences
63
83
}
64
84
65
85
findNotesRules :: Recorder (WithPriority Log ) -> Rules ()
@@ -69,20 +89,59 @@ findNotesRules recorder = do
69
89
70
90
defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \ MkGetNotes _ -> do
71
91
targets <- toKnownFiles <$> useNoFile_ GetKnownTargets
72
- definedNotes <- catMaybes <$> mapM (\ nfp -> fmap (HM. map (nfp,)) <$> use MkGetNotesInFile nfp) (HS. toList targets)
92
+ definedNotes <- catMaybes <$> mapM (\ nfp -> fmap (HM. map (nfp,) . fst ) <$> use MkGetNotesInFile nfp) (HS. toList targets)
73
93
pure $ Just $ HM. unions definedNotes
74
94
95
+ defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \ MkGetNoteReferences _ -> do
96
+ targets <- toKnownFiles <$> useNoFile_ GetKnownTargets
97
+ definedReferences <- catMaybes <$> for (HS. toList targets) (\ nfp -> do
98
+ references <- fmap snd <$> use MkGetNotesInFile nfp
99
+ pure $ fmap (HM. map (fmap (nfp,))) references
100
+ )
101
+ pure $ Just $ foldl' (HM. unionWith (<>) ) HM. empty definedReferences
102
+
103
+ err :: MonadError PluginError m => Text -> Maybe a -> m a
104
+ err s = maybe (throwError $ PluginInternalError s) pure
105
+
106
+ getNote :: NormalizedFilePath -> IdeState -> Position -> ExceptT PluginError (HandlerM c ) (Maybe Text )
107
+ getNote nfp state (Position l c) = do
108
+ contents <-
109
+ err " Error getting file contents"
110
+ =<< liftIO (runAction " notes.getfileContents" state (getFileContents nfp))
111
+ line <- err " Line not found in file" (listToMaybe $ Rope. lines $ fst
112
+ (Rope. splitAtLine 1 $ snd $ Rope. splitAtLine (fromIntegral l) contents))
113
+ pure $ listToMaybe $ mapMaybe (atPos $ fromIntegral c) $ matchAllText noteRefRegex line
114
+ where
115
+ atPos c arr = case arr A. ! 0 of
116
+ -- We check if the line we are currently at contains a note
117
+ -- reference. However, we need to know if the cursor is within the
118
+ -- match or somewhere else. The second entry of the array contains
119
+ -- the title of the note as extracted by the regex.
120
+ (_, (c', len)) -> if c' <= c && c <= c' + len
121
+ then Just (fst (arr A. ! 1 )) else Nothing
122
+
123
+ listReferences :: PluginMethodHandler IdeState Method_TextDocumentReferences
124
+ listReferences state _ param
125
+ | Just nfp <- uriToNormalizedFilePath uriOrig
126
+ = do
127
+ let pos@ (Position l _) = param ^. L. position
128
+ noteOpt <- getNote nfp state pos
129
+ case noteOpt of
130
+ Nothing -> pure (InR Null )
131
+ Just note -> do
132
+ notes <- runActionE " notes.definedNoteReferencess" state $ useE MkGetNoteReferences nfp
133
+ poss <- err (" Note reference (a comment of the form `{- Note [" <> note <> " ] -}`) not found" ) (HM. lookup note notes)
134
+ pure $ InL (mapMaybe (\ (noteFp, pos@ (Position l' _)) -> if l' == l then Nothing else Just (
135
+ Location (fromNormalizedUri $ normalizedFilePathToUri noteFp) (Range pos pos))) poss)
136
+ where
137
+ uriOrig = toNormalizedUri $ param ^. (L. textDocument . L. uri)
138
+ listReferences _ _ _ = throwError $ PluginInternalError " conversion to normalized file path failed"
139
+
75
140
jumpToNote :: PluginMethodHandler IdeState Method_TextDocumentDefinition
76
141
jumpToNote state _ param
77
142
| Just nfp <- uriToNormalizedFilePath uriOrig
78
143
= do
79
- let Position l c = param ^. L. position
80
- contents <-
81
- err " Error getting file contents"
82
- =<< liftIO (runAction " notes.getfileContents" state (getFileContents nfp))
83
- line <- err " Line not found in file" (listToMaybe $ Rope. lines $ fst
84
- (Rope. splitAtLine 1 $ snd $ Rope. splitAtLine (fromIntegral l) contents))
85
- let noteOpt = listToMaybe $ mapMaybe (atPos $ fromIntegral c) $ matchAllText noteRefRegex line
144
+ noteOpt <- getNote nfp state (param ^. L. position)
86
145
case noteOpt of
87
146
Nothing -> pure (InR (InR Null ))
88
147
Just note -> do
@@ -93,28 +152,23 @@ jumpToNote state _ param
93
152
))
94
153
where
95
154
uriOrig = toNormalizedUri $ param ^. (L. textDocument . L. uri)
96
- err s = maybe (throwError $ PluginInternalError s) pure
97
- atPos c arr = case arr A. ! 0 of
98
- -- We check if the line we are currently at contains a note
99
- -- reference. However, we need to know if the cursor is within the
100
- -- match or somewhere else. The second entry of the array contains
101
- -- the title of the note as extracted by the regex.
102
- (_, (c', len)) -> if c' <= c && c <= c' + len
103
- then Just (fst (arr A. ! 1 )) else Nothing
104
155
jumpToNote _ _ _ = throwError $ PluginInternalError " conversion to normalized file path failed"
105
156
106
- findNotesInFile :: NormalizedFilePath -> Recorder (WithPriority Log ) -> Action (Maybe (HM. HashMap Text Position ))
157
+ findNotesInFile :: NormalizedFilePath -> Recorder (WithPriority Log ) -> Action (Maybe (HM. HashMap Text Position , HM. HashMap Text [ Position ] ))
107
158
findNotesInFile file recorder = do
108
159
-- GetFileContents only returns a value if the file is open in the editor of
109
160
-- the user. If not, we need to read it from disk.
110
161
contentOpt <- (snd =<< ) <$> use GetFileContents file
111
162
content <- case contentOpt of
112
163
Just x -> pure $ Rope. toText x
113
164
Nothing -> liftIO $ readFileUtf8 $ fromNormalizedFilePath file
114
- let matches = (A. ! 1 ) <$> matchAllText noteRegex content
115
- m = toPositions matches content
116
- logWith recorder Debug $ LogNotesFound file (HM. toList m)
117
- pure $ Just m
165
+ let noteMatches = (A. ! 1 ) <$> matchAllText noteRegex content
166
+ notes = toPositions noteMatches content
167
+ logWith recorder Debug $ LogNotesFound file (HM. toList notes)
168
+ let refMatches = (A. ! 1 ) <$> matchAllText noteRefRegex content
169
+ refs = toPositions refMatches content
170
+ logWith recorder Debug $ LogNoteReferencesFound file (HM. toList refs)
171
+ pure $ Just (HM. mapMaybe (fmap fst . uncons) notes, refs)
118
172
where
119
173
uint = fromIntegral . toInteger
120
174
-- the regex library returns the character index of the match. However
@@ -129,7 +183,7 @@ findNotesInFile file recorder = do
129
183
let ! c' = c + 1
130
184
(! n', ! nc') = if char' == ' \n ' then (n + 1 , c') else (n, nc)
131
185
p@ (! _, ! _) = if char == c then
132
- (xs, HM. insert name ( Position (uint n') (uint (char - nc'))) m)
186
+ (xs, HM. insertWith (<>) name [ Position (uint n') (uint (char - nc'))] m)
133
187
else (x: xs, m)
134
188
in (p, (n', nc', c'))
135
189
) ((matches, HM. empty), (0 , 0 , 0 ))
0 commit comments