22{-# LANGUAGE  DeriveGeneric #-}
33{-# LANGUAGE  DeriveAnyClass #-}
44{-# LANGUAGE  TupleSections #-}
5+ {-# LANGUAGE  LambdaCase #-}
56module  Haskell.Ide.Engine.Plugin.HsImport  where 
67
78import            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. 
4549data  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
5156importCmd  ::  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: " $  \ 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: " $  \ 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 " " 
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<-  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
89108
90109                Just  (_, provider) ->  do 
91-                   let  formatEdit ::  J. TextEdit->  IdeGhcM  J. TextEdit
92-                       formatEdit origEdit@ (J. TextEdit=  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=  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.  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
111154            else  return  $  IdeResultOk  (J. WorkspaceEdit
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. 
113166codeActionProvider  ::  CodeActionProvider 
114167codeActionProvider plId docId _ context =  do 
115168  let  J. List=  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" Just  cmdParams)
136-       return  (Just  (codeAction cmd))
137-      where 
138-        codeAction cmd =  J. CodeActionJust  J. CodeActionQuickFixJust  (J. ListNothing  (Just  cmd)
139-        title =  " Import module " <>  modName
140-        cmdParams =  [toJSON (ImportParams  (docId ^.  J. uri) modName)]
141- 
142-     getImportables  ::  J. Diagnostic->  Maybe J. DiagnosticT. Text
143-     getImportables diag@ (J. DiagnosticJust  " ghcmod" =  (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. DiagnosticT. 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. DiagnosticT. 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" Just  cmdParams)
243+     return  (Just  (codeAction cmd))
244+    where 
245+     codeAction cmd =  J. CodeAction
246+                                   (Just  J. CodeActionQuickFix
247+                                   (Just  (J. List
248+                                   Nothing 
249+                                   (Just  cmd)
250+     title = 
251+       " Import module " 
252+         <>  modName
253+         <>  maybe  " " \ name ->  "  (" <>  name <>  " )" 
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. DiagnosticT. Text
260+   getImportables diag@ (J. DiagnosticJust  " ghcmod" = 
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. 
146268extractImportableTerm  ::  T. Text->  Maybe T. Text
147269extractImportableTerm dirtyMsg =  T. strip <$>  asum
148270  [ T. stripPrefix " Variable not in scope: " 
149271  , T. init  <$>  T. stripPrefix " Not in scope: type constructor or class ‘" 
150-   , T. stripPrefix " Data constructor not in scope: " 
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 " • " " " 
272+   , T. stripPrefix " Data constructor not in scope: " 
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 " • " " " 
0 commit comments