2
2
{-# LANGUAGE DeriveGeneric #-}
3
3
{-# LANGUAGE DeriveAnyClass #-}
4
4
{-# LANGUAGE TupleSections #-}
5
+ {-# LANGUAGE LambdaCase #-}
5
6
module Haskell.Ide.Engine.Plugin.HsImport where
6
7
7
8
import Control.Lens.Operators
@@ -42,60 +43,102 @@ hsimportDescriptor plId = PluginDescriptor
42
43
, pluginFormattingProvider = Nothing
43
44
}
44
45
46
+ -- | Import Parameters for Modules.
47
+ -- Can be used to import every symbol from a module,
48
+ -- or to import only a specific function from a module.
45
49
data ImportParams = ImportParams
46
- { file :: Uri
47
- , moduleToImport :: T. Text
50
+ { file :: Uri -- ^ Uri to the file to import the module to.
51
+ , addToImportList :: Maybe T. Text -- ^ If set, an import-list will be created.
52
+ , moduleToImport :: T. Text -- ^ Name of the module to import.
48
53
}
49
54
deriving (Show , Eq , Generics.Generic , ToJSON , FromJSON )
50
55
51
56
importCmd :: CommandFunc ImportParams J. WorkspaceEdit
52
- importCmd = CmdSync $ \ (ImportParams uri modName) -> importModule uri modName
57
+ importCmd = CmdSync $ \ (ImportParams uri importList modName) ->
58
+ importModule uri importList modName
53
59
54
- importModule :: Uri -> T. Text -> IdeGhcM (IdeResult J. WorkspaceEdit )
55
- importModule uri modName = pluginGetFile " hsimport cmd: " uri $ \ origInput -> do
60
+ -- | Import the given module for the given file.
61
+ -- May take an explicit function name to perform an import-list import.
62
+ -- Multiple import-list imports will result in merged imports,
63
+ -- e.g. two consecutive imports for the same module will result in a single
64
+ -- import line.
65
+ importModule
66
+ :: Uri -> Maybe T. Text -> T. Text -> IdeGhcM (IdeResult J. WorkspaceEdit )
67
+ importModule uri importList modName =
68
+ pluginGetFile " hsimport cmd: " uri $ \ origInput -> do
56
69
shouldFormat <- formatOnImportOn <$> getConfig
57
-
58
70
fileMap <- GM. mkRevRedirMapFunc
59
71
GM. withMappedFile origInput $ \ input -> do
60
72
61
73
tmpDir <- liftIO getTemporaryDirectory
62
74
(output, outputH) <- liftIO $ openTempFile tmpDir " hsimportOutput"
63
75
liftIO $ hClose outputH
64
-
65
76
let args = defaultArgs { moduleName = T. unpack modName
66
77
, inputSrcFile = input
78
+ , symbolName = T. unpack $ fromMaybe " " importList
67
79
, outputSrcFile = output
68
80
}
81
+ -- execute hsimport on the given file and write into a temporary file.
69
82
maybeErr <- liftIO $ hsimportWithArgs defaultConfig args
70
83
case maybeErr of
71
84
Just err -> do
72
85
liftIO $ removeFile output
73
86
let msg = T. pack $ show err
74
87
return $ IdeResultFail (IdeError PluginError msg Null )
75
88
Nothing -> do
89
+ -- Since no error happened, calculate the differences of
90
+ -- the original file and after the import has been done.
76
91
newText <- liftIO $ T. readFile output
77
92
liftIO $ removeFile output
78
93
J. WorkspaceEdit mChanges mDocChanges <- liftToGhc
79
94
$ makeDiffResult input newText fileMap
80
95
96
+ -- If the client wants its import formatted,
97
+ -- it can be configured in the config.
81
98
if shouldFormat
82
99
then do
83
100
config <- getConfig
84
101
plugins <- getPlugins
85
102
let mprovider = Hie. getFormattingPlugin config plugins
86
103
case mprovider of
104
+ -- Client may have no formatter selected
105
+ -- but still the option to format on import.
87
106
Nothing ->
88
107
return $ IdeResultOk (J. WorkspaceEdit mChanges mDocChanges)
89
108
90
109
Just (_, provider) -> do
91
- let formatEdit :: J. TextEdit -> IdeGhcM J. TextEdit
92
- formatEdit origEdit@ (J. TextEdit _ t) = do
110
+ let
111
+ -- | Dirty little hack.
112
+ -- Necessary in the following case:
113
+ -- We want to add an item to an existing import-list.
114
+ -- The diff algorithm does not count the newline character
115
+ -- as part of the diff between new and old text.
116
+ -- However, some formatters (Brittany), add a trailing
117
+ -- newline nevertheless.
118
+ -- This leads to the problem that an additional
119
+ -- newline is inserted into the source.
120
+ -- This function makes sure, that if the original text
121
+ -- did not have a newline, none will be added, assuming
122
+ -- that the diff algorithm continues to not count newlines
123
+ -- as part of the diff.
124
+ -- This is only save to do in this very specific environment.
125
+ -- In any other case, this function may not be copy-pasted
126
+ -- to solve a similar problem.
127
+ renormalise :: T. Text -> T. Text -> T. Text
128
+ renormalise orig formatted
129
+ | T. null orig || T. null formatted = orig <> formatted
130
+ | T. last orig /= ' \n ' && T. last formatted == ' \n ' = T. init formatted
131
+ | otherwise = formatted
132
+
133
+ formatEdit :: J. TextEdit -> IdeGhcM J. TextEdit
134
+ formatEdit origEdit@ (J. TextEdit r t) = do
93
135
-- TODO: are these default FormattingOptions ok?
94
- res <- liftToGhc $ provider t uri FormatDocument (FormattingOptions 2 True )
95
- let formatEdits = case res of
96
- IdeResultOk xs -> xs
97
- _ -> []
98
- return $ foldl' J. editTextEdit origEdit formatEdits
136
+ formatEdits <-
137
+ liftToGhc $ provider t uri FormatText (FormattingOptions 2 True ) >>= \ case
138
+ IdeResultOk xs -> return xs
139
+ _ -> return [origEdit]
140
+ -- let edits = foldl' J.editTextEdit origEdit formatEdits -- TODO: this seems broken.
141
+ return (J. TextEdit r (renormalise t . J. _newText $ head formatEdits))
99
142
100
143
-- behold: the legendary triple mapM
101
144
newChanges <- (mapM . mapM . mapM ) formatEdit mChanges
@@ -110,48 +153,132 @@ importModule uri modName = pluginGetFile "hsimport cmd: " uri $ \origInput -> do
110
153
$ IdeResultOk (J. WorkspaceEdit newChanges newDocChanges)
111
154
else return $ IdeResultOk (J. WorkspaceEdit mChanges mDocChanges)
112
155
156
+ -- | Search style for Hoogle.
157
+ -- Can be used to look either for the exact term,
158
+ -- only the exact name or a relaxed form of the term.
159
+ data SearchStyle
160
+ = Exact -- ^ If you want to match exactly the search string.
161
+ | ExactName -- ^ If you want to match exactly a function name.
162
+ -- Same as @Exact@ if the term is just a function name.
163
+ | Relax (T. Text -> T. Text ) -- ^ Relax the search term to match even more.
164
+
165
+ -- | Produces code actions.
113
166
codeActionProvider :: CodeActionProvider
114
167
codeActionProvider plId docId _ context = do
115
168
let J. List diags = context ^. J. diagnostics
116
- terms = mapMaybe getImportables diags
117
-
118
- res <- mapM (bimapM return Hoogle. searchModules) terms
119
- actions <- catMaybes <$> mapM (uncurry mkImportAction) (concatTerms res)
169
+ terms = mapMaybe getImportables diags
170
+ -- Search for the given diagnostics and produce appropiate import actions.
171
+ actions <- importActionsForTerms Exact terms
120
172
121
173
if null actions
122
- then do
123
- let relaxedTerms = map (bimap id (head . T. words )) terms
124
- relaxedRes <- mapM (bimapM return Hoogle. searchModules) relaxedTerms
125
- relaxedActions <- catMaybes <$> mapM (uncurry mkImportAction) (concatTerms relaxedRes)
126
- return $ IdeResultOk relaxedActions
127
- else return $ IdeResultOk actions
128
-
129
- where
130
- concatTerms = concatMap (\ (d, ts) -> map (d,) ts)
131
-
132
- -- TODO: Check if package is already installed
133
- mkImportAction :: J. Diagnostic -> T. Text -> IdeM (Maybe J. CodeAction )
134
- mkImportAction diag modName = do
135
- cmd <- mkLspCommand plId " import" title (Just cmdParams)
136
- return (Just (codeAction cmd))
137
- where
138
- codeAction cmd = J. CodeAction title (Just J. CodeActionQuickFix ) (Just (J. List [diag])) Nothing (Just cmd)
139
- title = " Import module " <> modName
140
- cmdParams = [toJSON (ImportParams (docId ^. J. uri) modName)]
141
-
142
- getImportables :: J. Diagnostic -> Maybe (J. Diagnostic , T. Text )
143
- getImportables diag@ (J. Diagnostic _ _ _ (Just " ghcmod" ) msg _) = (diag,) <$> extractImportableTerm msg
144
- getImportables _ = Nothing
174
+ then do
175
+ -- If we didn't find any exact matches, relax the search terms.
176
+ -- Only looks for the function names, not the exact siganture.
177
+ relaxedActions <- importActionsForTerms ExactName terms
178
+ return $ IdeResultOk relaxedActions
179
+ else return $ IdeResultOk actions
180
+
181
+ where
182
+ -- | Creates CodeActions from the diagnostics to add imports.
183
+ -- Takes a relaxation Function. Used to relax the search term,
184
+ -- e.g. instead of `take :: Int -> [a] -> [a]` use `take` as the search term.
185
+ --
186
+ -- List of Diagnostics with the associated term to look for.
187
+ -- Diagnostic that is supposed to import the appropiate term.
188
+ --
189
+ -- Result may produce several import actions, or none.
190
+ importActionsForTerms
191
+ :: SearchStyle -> [(J. Diagnostic , T. Text )] -> IdeM [J. CodeAction ]
192
+ importActionsForTerms style terms = do
193
+ let searchTerms = map (bimap id (applySearchStyle style)) terms
194
+ -- Get the function names for a nice import-list title.
195
+ let functionNames = map (head . T. words . snd ) terms
196
+ searchResults' <- mapM (bimapM return Hoogle. searchModules) searchTerms
197
+ let searchResults = zip functionNames searchResults'
198
+ let normalise =
199
+ concatMap (\ (a, b) -> zip (repeat a) (concatTerms b)) searchResults
200
+
201
+ concat <$> mapM (uncurry (termToActions style)) normalise
202
+
203
+ -- | Apply the search style to given term.
204
+ -- Can be used to look for a term that matches exactly the search term,
205
+ -- or one that matches only the exact name.
206
+ -- At last, a custom relaxation function can be passed for more control.
207
+ applySearchStyle :: SearchStyle -> T. Text -> T. Text
208
+ applySearchStyle Exact term = " is:exact " <> term
209
+ applySearchStyle ExactName term = case T. words term of
210
+ [] -> term
211
+ (x : _) -> " is:exact " <> x
212
+ applySearchStyle (Relax relax) term = relax term
213
+
214
+ -- | Turn a search term with function name into Import Actions.
215
+ -- Function name may be of only the exact phrase to import.
216
+ -- The resulting CodeAction's contain a general import of a module or
217
+ -- uses an Import-List.
218
+ --
219
+ -- Note, that repeated use of the Import-List will add imports to
220
+ -- the appropriate import line, e.g. no module import is duplicated, except
221
+ -- for qualified imports.
222
+ --
223
+ -- If the search term is relaxed in a custom way,
224
+ -- no import list can be offered, since the function name
225
+ -- may be not the one we expect.
226
+ termToActions
227
+ :: SearchStyle -> T. Text -> (J. Diagnostic , T. Text ) -> IdeM [J. CodeAction ]
228
+ termToActions style functionName (diagnostic, termName) = do
229
+ let useImportList = case style of
230
+ Relax _ -> Nothing
231
+ _ -> Just (mkImportAction (Just functionName) diagnostic termName)
232
+ catMaybes <$> sequenceA
233
+ (mkImportAction Nothing diagnostic termName : maybeToList useImportList)
234
+
235
+ concatTerms :: (a , [b ]) -> [(a , b )]
236
+ concatTerms (a, b) = zip (repeat a) b
237
+
238
+ -- TODO: Check if package is already installed
239
+ mkImportAction
240
+ :: Maybe T. Text -> J. Diagnostic -> T. Text -> IdeM (Maybe J. CodeAction )
241
+ mkImportAction importList diag modName = do
242
+ cmd <- mkLspCommand plId " import" title (Just cmdParams)
243
+ return (Just (codeAction cmd))
244
+ where
245
+ codeAction cmd = J. CodeAction title
246
+ (Just J. CodeActionQuickFix )
247
+ (Just (J. List [diag]))
248
+ Nothing
249
+ (Just cmd)
250
+ title =
251
+ " Import module "
252
+ <> modName
253
+ <> maybe " " (\ name -> " (" <> name <> " )" ) importList
254
+ cmdParams = [toJSON (ImportParams (docId ^. J. uri) importList modName)]
255
+
256
+
257
+ -- | For a Diagnostic, get an associated function name.
258
+ -- If Ghc-Mod can not find any candidates, Nothing is returned.
259
+ getImportables :: J. Diagnostic -> Maybe (J. Diagnostic , T. Text )
260
+ getImportables diag@ (J. Diagnostic _ _ _ (Just " ghcmod" ) msg _) =
261
+ (diag, ) <$> extractImportableTerm msg
262
+ getImportables _ = Nothing
145
263
264
+ -- | Extract from an error message an appropriate term to search for.
265
+ -- This looks at the error message and tries to extract the expected
266
+ -- signature of an unknown function.
267
+ -- If this is not possible, Nothing is returned.
146
268
extractImportableTerm :: T. Text -> Maybe T. Text
147
269
extractImportableTerm dirtyMsg = T. strip <$> asum
148
270
[ T. stripPrefix " Variable not in scope: " msg
149
271
, T. init <$> T. stripPrefix " Not in scope: type constructor or class ‘" msg
150
- , T. stripPrefix " Data constructor not in scope: " msg]
151
- where msg = head
152
- -- Get rid of the rename suggestion parts
153
- $ T. splitOn " Perhaps you meant "
154
- $ T. replace " \n " " "
155
- -- Get rid of trailing/leading whitespace on each individual line
156
- $ T. unlines $ map T. strip $ T. lines
157
- $ T. replace " • " " " dirtyMsg
272
+ , T. stripPrefix " Data constructor not in scope: " msg
273
+ ]
274
+ where
275
+ msg =
276
+ head
277
+ -- Get rid of the rename suggestion parts
278
+ $ T. splitOn " Perhaps you meant "
279
+ $ T. replace " \n " " "
280
+ -- Get rid of trailing/leading whitespace on each individual line
281
+ $ T. unlines
282
+ $ map T. strip
283
+ $ T. lines
284
+ $ T. replace " • " " " dirtyMsg
0 commit comments