Skip to content
This repository was archived by the owner on Oct 7, 2020. It is now read-only.

Commit 69878c9

Browse files
authored
Merge pull request #1170 from fendor/hsimport-importlist
HsImport importlist
2 parents cd9f578 + 049609d commit 69878c9

File tree

9 files changed

+441
-126
lines changed

9 files changed

+441
-126
lines changed

haskell-ide-engine.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ test-suite func-test
283283
, lens
284284
, text
285285
, unordered-containers
286+
, containers
286287
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Wredundant-constraints
287288
if flag(pedantic)
288289
ghc-options: -Werror

hie-plugin-api/Haskell/Ide/Engine/PluginsIdeMonads.hs

+14-6
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,18 @@ type HoverProvider = Uri -> Position -> IdeM (IdeResult [Hover])
207207

208208
type SymbolProvider = Uri -> IdeDeferM (IdeResult [DocumentSymbol])
209209

210-
-- | Format the document either as a whole or only a given Range of it.
211-
data FormattingType = FormatDocument
210+
-- | Format the given Text as a whole or only a @Range@ of it.
211+
-- Range must be relative to the text to format.
212+
-- To format the whole document, read the Text from the file and use 'FormatText'
213+
-- as the FormattingType.
214+
data FormattingType = FormatText
212215
| FormatRange Range
213216

214217
-- | Formats the given Text associated with the given Uri.
215-
-- Should, but might not, honor the provided formatting options (e.g. Floskell does not).
216-
-- A formatting type can be given to either format the whole document or only a Range.
217-
--
218-
-- Text to format, may or may not, originate from the associated Uri.
218+
-- Should, but might not, honour the provided formatting options (e.g. Floskell does not).
219+
-- A formatting type can be given to either format the whole text or only a Range.
220+
--
221+
-- Text to format, may or may not, originate from the associated Uri.
219222
-- E.g. it is ok, to modify the text and then reformat it through this API.
220223
--
221224
-- The Uri is mainly used to discover formatting configurations in the file's path.
@@ -224,6 +227,11 @@ data FormattingType = FormatDocument
224227
-- Failing means here that a IdeResultFail is returned.
225228
-- This can be used to display errors to the user, unless the error is an Internal one.
226229
-- The record 'IdeError' and 'IdeErrorCode' can be used to determine the type of error.
230+
--
231+
--
232+
-- To format a whole document, the 'FormatText' @FormattingType@ can be used.
233+
-- It is required to pass in the whole Document Text for that to happen, an empty text
234+
-- and file uri, does not suffice.
227235
type FormattingProvider = T.Text -- ^ Text to format
228236
-> Uri -- ^ Uri of the file being formatted
229237
-> FormattingType -- ^ How much to format

src/Haskell/Ide/Engine/Plugin/Brittany.hs

+13-6
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ provider
4343
provider text uri formatType opts = pluginGetFile "brittanyCmd: " uri $ \fp -> do
4444
confFile <- liftIO $ getConfFile fp
4545
let (range, selectedContents) = case formatType of
46-
FormatDocument -> (fullRange text, text)
47-
FormatRange r -> (normalize r, extractRange r text)
46+
FormatText -> (fullRange text, text)
47+
FormatRange r -> (normalize r, extractRange r text)
4848

4949
res <- formatText confFile opts selectedContents
5050
case res of
@@ -65,21 +65,28 @@ formatText
6565
=> Maybe FilePath -- ^ Path to configs. If Nothing, default configs will be used.
6666
-> FormattingOptions -- ^ Options for the formatter such as indentation.
6767
-> Text -- ^ Text to format
68-
-> m (Either [BrittanyError] Text) -- ^ Either formatted Text or a error from Brittany.
69-
formatText confFile opts text =
68+
-> m (Either [BrittanyError] Text) -- ^ Either formatted Text or a error from Brittany.
69+
formatText confFile opts text =
7070
liftIO $ runBrittany tabSize confFile text
7171
where tabSize = opts ^. J.tabSize
7272

73-
-- | Extend to the line below to replace newline character, as above.
73+
-- | Extend to the line below and above to replace newline character.
7474
normalize :: Range -> Range
7575
normalize (Range (Position sl _) (Position el _)) =
7676
Range (Position sl 0) (Position (el + 1) 0)
7777

78-
-- | Recursively search in every directory of the given filepath for brittany.yaml
78+
-- | Recursively search in every directory of the given filepath for brittany.yaml.
7979
-- If no such file has been found, return Nothing.
8080
getConfFile :: FilePath -> IO (Maybe FilePath)
8181
getConfFile = findLocalConfigPath . takeDirectory
8282

83+
-- | Run Brittany on the given text with the given tab size and
84+
-- a configuration path. If no configuration path is given, a
85+
-- default configuration is chosen. The configuration may overwrite
86+
-- tab size parameter.
87+
--
88+
-- Returns either a list of Brittany Errors or the reformatted text.
89+
-- May not throw an exception.
8390
runBrittany :: Int -- ^ tab size
8491
-> Maybe FilePath -- ^ local config file
8592
-> Text -- ^ text to format

src/Haskell/Ide/Engine/Plugin/Floskell.hs

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ provider contents uri typ _opts =
3535
pluginGetFile "Floskell: " uri $ \file -> do
3636
config <- liftIO $ findConfigOrDefault file
3737
let (range, selectedContents) = case typ of
38-
FormatDocument -> (fullRange contents, contents)
39-
FormatRange r -> (r, extractRange r contents)
40-
result = reformat config (uriToFilePath uri) (BS.fromStrict (T.encodeUtf8 selectedContents))
38+
FormatText -> (fullRange contents, contents)
39+
FormatRange r -> (r, extractRange r contents)
40+
result = reformat config (Just file) (BS.fromStrict (T.encodeUtf8 selectedContents))
4141
case result of
4242
Left err -> return $ IdeResultFail (IdeError PluginError (T.pack $ "floskellCmd: " ++ err) Null)
4343
Right new -> return $ IdeResultOk [TextEdit range (T.decodeUtf8 (BS.toStrict new))]

src/Haskell/Ide/Engine/Plugin/HsImport.hs

+176-49
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{-# LANGUAGE DeriveGeneric #-}
33
{-# LANGUAGE DeriveAnyClass #-}
44
{-# LANGUAGE TupleSections #-}
5+
{-# LANGUAGE LambdaCase #-}
56
module Haskell.Ide.Engine.Plugin.HsImport where
67

78
import Control.Lens.Operators
@@ -42,60 +43,102 @@ hsimportDescriptor plId = PluginDescriptor
4243
, pluginFormattingProvider = Nothing
4344
}
4445

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.
4549
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.
4853
}
4954
deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON)
5055

5156
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
5359

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
5669
shouldFormat <- formatOnImportOn <$> getConfig
57-
5870
fileMap <- GM.mkRevRedirMapFunc
5971
GM.withMappedFile origInput $ \input -> do
6072

6173
tmpDir <- liftIO getTemporaryDirectory
6274
(output, outputH) <- liftIO $ openTempFile tmpDir "hsimportOutput"
6375
liftIO $ hClose outputH
64-
6576
let args = defaultArgs { moduleName = T.unpack modName
6677
, inputSrcFile = input
78+
, symbolName = T.unpack $ fromMaybe "" importList
6779
, outputSrcFile = output
6880
}
81+
-- execute hsimport on the given file and write into a temporary file.
6982
maybeErr <- liftIO $ hsimportWithArgs defaultConfig args
7083
case maybeErr of
7184
Just err -> do
7285
liftIO $ removeFile output
7386
let msg = T.pack $ show err
7487
return $ IdeResultFail (IdeError PluginError msg Null)
7588
Nothing -> do
89+
-- Since no error happened, calculate the differences of
90+
-- the original file and after the import has been done.
7691
newText <- liftIO $ T.readFile output
7792
liftIO $ removeFile output
7893
J.WorkspaceEdit mChanges mDocChanges <- liftToGhc
7994
$ makeDiffResult input newText fileMap
8095

96+
-- If the client wants its import formatted,
97+
-- it can be configured in the config.
8198
if shouldFormat
8299
then do
83100
config <- getConfig
84101
plugins <- getPlugins
85102
let mprovider = Hie.getFormattingPlugin config plugins
86103
case mprovider of
104+
-- Client may have no formatter selected
105+
-- but still the option to format on import.
87106
Nothing ->
88107
return $ IdeResultOk (J.WorkspaceEdit mChanges mDocChanges)
89108

90109
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
93135
-- 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))
99142

100143
-- behold: the legendary triple mapM
101144
newChanges <- (mapM . mapM . mapM) formatEdit mChanges
@@ -110,48 +153,132 @@ importModule uri modName = pluginGetFile "hsimport cmd: " uri $ \origInput -> do
110153
$ IdeResultOk (J.WorkspaceEdit newChanges newDocChanges)
111154
else return $ IdeResultOk (J.WorkspaceEdit mChanges mDocChanges)
112155

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.
113166
codeActionProvider :: CodeActionProvider
114167
codeActionProvider plId docId _ context = do
115168
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
120172

121173
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
145263

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.
146268
extractImportableTerm :: T.Text -> Maybe T.Text
147269
extractImportableTerm dirtyMsg = T.strip <$> asum
148270
[ T.stripPrefix "Variable not in scope: " msg
149271
, 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

Comments
 (0)