From 3350686dc36c9ed012a817b6806ec1a34c4b2d05 Mon Sep 17 00:00:00 2001 From: fendor Date: Mon, 3 Jun 2019 13:09:45 +0200 Subject: [PATCH 01/12] Clone HsImport API for more fine grained control Enables to use imports of constructors --- src/Haskell/Ide/Engine/Plugin/Hoogle.hs | 4 +- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 236 ++++++++++++++++------ 2 files changed, 180 insertions(+), 60 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs index 2dd279ab1..de8401cc5 100644 --- a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs +++ b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs @@ -44,10 +44,10 @@ hoogleDescriptor plId = PluginDescriptor -- --------------------------------------------------------------------- -data HoogleError +data HoogleError = NoDb | DbFail T.Text - | NoResults + | NoResults deriving (Eq,Ord,Show) newtype HoogleDb = HoogleDb (Maybe FilePath) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index 1737e66f9..0a5a663f6 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -1,7 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE TupleSections #-} {-# LANGUAGE LambdaCase #-} module Haskell.Ide.Engine.Plugin.HsImport where @@ -9,8 +8,6 @@ import Control.Lens.Operators import Control.Monad.IO.Class import Control.Monad import Data.Aeson -import Data.Bitraversable -import Data.Bifunctor import Data.Foldable import Data.Maybe import Data.Monoid ( (<>) ) @@ -18,7 +15,7 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import qualified GHC.Generics as Generics import qualified GhcModCore as GM ( mkRevRedirMapFunc, withMappedFile ) -import HsImport +import qualified HsImport import Haskell.Ide.Engine.Config import Haskell.Ide.Engine.MonadTypes import qualified Haskell.Ide.Engine.Support.HieExtras as Hie @@ -29,6 +26,7 @@ import qualified Haskell.Ide.Engine.Plugin.Hoogle as Hoogle import System.Directory import System.IO +import qualified Safe as S hsimportDescriptor :: PluginId -> PluginDescriptor hsimportDescriptor plId = PluginDescriptor @@ -43,19 +41,61 @@ hsimportDescriptor plId = PluginDescriptor , pluginFormattingProvider = Nothing } +data SymbolType + = Symbol + | Constructor + | Type + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + + +-- | What of the symbol should be taken. +data SymbolKind + = Only SymbolName -- ^ only the symbol should be taken + | AllOf DatatypeName -- ^ all constructors or methods of the symbol should be taken: Symbol(..) + | OneOf DatatypeName SymbolName -- ^ some constructors or methods of the symbol should be taken: Symbol(X, Y) + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + +-- | The imported or from the import hidden symbol. +data SymbolImport a + = Import a -- ^ the symbol to import + | Hiding a -- ^ the symbol to hide from the import + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + + +extractSymbolImport :: SymbolImport a -> a +extractSymbolImport (Hiding s) = s +extractSymbolImport (Import s) = s + +type ModuleName = T.Text +type SymbolName = T.Text +type DatatypeName = T.Text + +data ImportStyle + = Simple -- ^ Import the whole module + | Complex (SymbolImport SymbolKind) -- ^ Complex operation, import module hiding symbols or import only selected symbols. + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + +data ImportDiagnostic = ImportDiagnostic + { diagnostic :: J.Diagnostic + , term :: SymbolName + , termType :: SymbolImport SymbolType + } + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + + -- | Import Parameters for Modules. -- Can be used to import every symbol from a module, -- or to import only a specific function from a module. data ImportParams = ImportParams - { file :: Uri -- ^ Uri to the file to import the module to. - , addToImportList :: Maybe T.Text -- ^ If set, an import-list will be created. - , moduleToImport :: T.Text -- ^ Name of the module to import. + { file :: Uri -- ^ Uri to the file to import the module to. + , importStyle :: ImportStyle -- ^ How to import the module + , moduleToImport :: ModuleName -- ^ Name of the module to import. } deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) importCmd :: CommandFunc ImportParams J.WorkspaceEdit -importCmd = CmdSync $ \(ImportParams uri importList modName) -> - importModule uri importList modName +importCmd = CmdSync $ \(ImportParams uri style modName) -> + importModule uri style modName -- | Import the given module for the given file. -- May take an explicit function name to perform an import-list import. @@ -63,8 +103,8 @@ importCmd = CmdSync $ \(ImportParams uri importList modName) -> -- e.g. two consecutive imports for the same module will result in a single -- import line. importModule - :: Uri -> Maybe T.Text -> T.Text -> IdeGhcM (IdeResult J.WorkspaceEdit) -importModule uri importList modName = + :: Uri -> ImportStyle -> ModuleName -> IdeGhcM (IdeResult J.WorkspaceEdit) +importModule uri impStyle modName = pluginGetFile "hsimport cmd: " uri $ \origInput -> do shouldFormat <- formatOnImportOn <$> getConfig fileMap <- GM.mkRevRedirMapFunc @@ -73,13 +113,9 @@ importModule uri importList modName = tmpDir <- liftIO getTemporaryDirectory (output, outputH) <- liftIO $ openTempFile tmpDir "hsimportOutput" liftIO $ hClose outputH - let args = defaultArgs { moduleName = T.unpack modName - , inputSrcFile = input - , symbolName = T.unpack $ fromMaybe "" importList - , outputSrcFile = output - } + let args = importStyleToHsImportArgs input output modName impStyle -- execute hsimport on the given file and write into a temporary file. - maybeErr <- liftIO $ hsimportWithArgs defaultConfig args + maybeErr <- liftIO $ HsImport.hsimportWithArgs HsImport.defaultConfig args case maybeErr of Just err -> do liftIO $ removeFile output @@ -153,6 +189,29 @@ importModule uri importList modName = $ IdeResultOk (J.WorkspaceEdit newChanges newDocChanges) else return $ IdeResultOk (J.WorkspaceEdit mChanges mDocChanges) +importStyleToHsImportArgs + :: FilePath -> FilePath -> ModuleName -> ImportStyle -> HsImport.HsImportArgs +importStyleToHsImportArgs input output modName style = + let defaultArgs = + HsImport.defaultArgs { HsImport.moduleName = T.unpack modName + , HsImport.inputSrcFile = input + , HsImport.outputSrcFile = output + } + kindToArgs kind = case kind of + Only sym -> defaultArgs { HsImport.symbolName = T.unpack sym } + OneOf dt sym -> defaultArgs { HsImport.symbolName = T.unpack dt + , HsImport.with = [T.unpack sym] + } + AllOf dt -> defaultArgs { HsImport.symbolName = T.unpack dt + , HsImport.all = True + } + in case style of + Simple -> defaultArgs + Complex s -> case s of + Hiding kind -> kindToArgs kind {- TODO: wait for hsimport version bump -} + Import kind -> kindToArgs kind + + -- | Search style for Hoogle. -- Can be used to look either for the exact term, -- only the exact name or a relaxed form of the term. @@ -188,28 +247,23 @@ codeActionProvider plId docId _ context = do -- -- Result may produce several import actions, or none. importActionsForTerms - :: SearchStyle -> [(J.Diagnostic, T.Text)] -> IdeM [J.CodeAction] - importActionsForTerms style terms = do - let searchTerms = map (bimap id (applySearchStyle style)) terms - -- Get the function names for a nice import-list title. - let functionNames = map (head . T.words . snd) terms - searchResults' <- mapM (bimapM return Hoogle.searchModules) searchTerms - let searchResults = zip functionNames searchResults' - let normalise = - concatMap (\(a, b) -> zip (repeat a) (concatTerms b)) searchResults - - concat <$> mapM (uncurry (termToActions style)) normalise + :: SearchStyle -> [ImportDiagnostic] -> IdeM [J.CodeAction] + importActionsForTerms style importDiagnostics = do + let searchTerms = map (applySearchStyle style . term) importDiagnostics + searchResults <- mapM Hoogle.searchModules searchTerms + let importTerms = zip searchResults importDiagnostics + concat <$> mapM (uncurry (termToActions style)) importTerms -- | Apply the search style to given term. -- Can be used to look for a term that matches exactly the search term, -- or one that matches only the exact name. -- At last, a custom relaxation function can be passed for more control. applySearchStyle :: SearchStyle -> T.Text -> T.Text - applySearchStyle Exact term = "is:exact " <> term - applySearchStyle ExactName term = case T.words term of - [] -> term + applySearchStyle Exact termName = "is:exact " <> termName + applySearchStyle ExactName termName = case T.words termName of + [] -> termName (x : _) -> "is:exact " <> x - applySearchStyle (Relax relax) term = relax term + applySearchStyle (Relax relax) termName = relax termName -- | Turn a search term with function name into Import Actions. -- Function name may be of only the exact phrase to import. @@ -224,55 +278,121 @@ codeActionProvider plId docId _ context = do -- no import list can be offered, since the function name -- may be not the one we expect. termToActions - :: SearchStyle -> T.Text -> (J.Diagnostic, T.Text) -> IdeM [J.CodeAction] - termToActions style functionName (diagnostic, termName) = do - let useImportList = case style of - Relax _ -> Nothing - _ -> Just (mkImportAction (Just functionName) diagnostic termName) - catMaybes <$> sequenceA - (mkImportAction Nothing diagnostic termName : maybeToList useImportList) + :: SearchStyle -> [ModuleName] -> ImportDiagnostic -> IdeM [J.CodeAction] + termToActions style modules impDiagnostic = + concat <$> mapM (importModuleAction style impDiagnostic) modules + + importModuleAction + :: SearchStyle -> ImportDiagnostic -> ModuleName -> IdeM [J.CodeAction] + importModuleAction searchStyle impDiagnostic moduleName = + catMaybes <$> sequenceA codeActions + where + importListActions :: [IdeM (Maybe J.CodeAction)] + importListActions = case searchStyle of + Relax _ -> [] + _ -> catMaybes + $ case extractSymbolImport $ termType impDiagnostic of + Symbol + -> [ mkImportAction moduleName impDiagnostic . Just . Only + <$> symName (term impDiagnostic) + ] + Constructor + -> [ mkImportAction moduleName impDiagnostic . Just . AllOf + <$> datatypeName (term impDiagnostic) + , (\dt sym -> mkImportAction moduleName impDiagnostic . Just + $ OneOf dt sym) + <$> datatypeName (term impDiagnostic) + <*> symName (term impDiagnostic) + ] + Type + -> [ mkImportAction moduleName impDiagnostic . Just . Only + <$> symName (term impDiagnostic)] + + codeActions :: [IdeM (Maybe J.CodeAction)] + codeActions = case termType impDiagnostic of + Hiding _ -> [] + Import _ -> [mkImportAction moduleName impDiagnostic Nothing] + ++ importListActions + + signatureOf :: T.Text -> Maybe T.Text + signatureOf sig = do + let parts = T.splitOn "::" sig + typeSig <- S.tailMay parts + S.headMay typeSig + + datatypeName :: T.Text -> Maybe T.Text + datatypeName sig = do + sig_ <- signatureOf sig + let sigParts = T.splitOn "->" sig_ + lastPart <- S.lastMay sigParts + let dtNameSig = T.words lastPart + qualifiedDtName <- S.headMay dtNameSig + let qualifiedDtNameParts = T.splitOn "." qualifiedDtName + S.lastMay qualifiedDtNameParts + + symName :: T.Text -> Maybe SymbolName + symName = S.headMay . T.words - concatTerms :: (a, [b]) -> [(a, b)] - concatTerms (a, b) = zip (repeat a) b --TODO: Check if package is already installed mkImportAction - :: Maybe T.Text -> J.Diagnostic -> T.Text -> IdeM (Maybe J.CodeAction) - mkImportAction importList diag modName = do + :: ModuleName -> ImportDiagnostic -> Maybe SymbolKind -> IdeM (Maybe J.CodeAction) + mkImportAction modName importDiagnostic symbolType = do cmd <- mkLspCommand plId "import" title (Just cmdParams) return (Just (codeAction cmd)) where codeAction cmd = J.CodeAction title (Just J.CodeActionQuickFix) - (Just (J.List [diag])) + (Just (J.List [diagnostic importDiagnostic])) Nothing (Just cmd) - title = - "Import module " - <> modName - <> maybe "" (\name -> " (" <> name <> ")") importList - cmdParams = [toJSON (ImportParams (docId ^. J.uri) importList modName)] + title = "Import module " + <> modName + <> case termType importDiagnostic of + Hiding _ -> "hiding " + Import _ -> "" + <> case symbolType of + Just s -> case s of + Only sym -> "(" <> sym <> ")" + AllOf dt -> "(" <> dt <> " (..))" + OneOf dt sym -> "(" <> dt <> " (" <> sym <> "))" + Nothing -> "" + + importStyleParam :: ImportStyle + importStyleParam = case symbolType of + Nothing -> Simple + Just k -> case termType importDiagnostic of + Hiding _ -> Complex (Hiding k) + Import _ -> Complex (Import k) + + cmdParams = [toJSON (ImportParams (docId ^. J.uri) importStyleParam modName)] -- | For a Diagnostic, get an associated function name. -- If Ghc-Mod can not find any candidates, Nothing is returned. - getImportables :: J.Diagnostic -> Maybe (J.Diagnostic, T.Text) + getImportables :: J.Diagnostic -> Maybe ImportDiagnostic getImportables diag@(J.Diagnostic _ _ _ (Just "ghcmod") msg _) = - (diag, ) <$> extractImportableTerm msg + uncurry (ImportDiagnostic diag) <$> extractImportableTerm msg getImportables _ = Nothing -- | Extract from an error message an appropriate term to search for. -- This looks at the error message and tries to extract the expected -- signature of an unknown function. -- If this is not possible, Nothing is returned. -extractImportableTerm :: T.Text -> Maybe T.Text -extractImportableTerm dirtyMsg = T.strip <$> asum - [ T.stripPrefix "Variable not in scope: " msg - , T.init <$> T.stripPrefix "Not in scope: type constructor or class ‘" msg - , T.stripPrefix "Data constructor not in scope: " msg - ] +extractImportableTerm :: T.Text -> Maybe (T.Text, (SymbolImport SymbolType) ) +extractImportableTerm dirtyMsg = + let extractedTerm = + asum + [ (\name -> (name, Import Symbol)) <$> T.stripPrefix "Variable not in scope: " importMsg + , (\name -> (T.init name, Import Type)) <$> T.stripPrefix "Not in scope: type constructor or class ‘" importMsg + , (\name -> (name, Import Constructor)) <$> T.stripPrefix "Data constructor not in scope: " importMsg + ] + in do + (n, s) <- extractedTerm + let n' = T.strip n + return (n', s) where - msg = + importMsg = head -- Get rid of the rename suggestion parts $ T.splitOn "Perhaps you meant " From 07822b472e0495a1819008362e53ad07e670fdc6 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 13:25:30 +0200 Subject: [PATCH 02/12] Add documentation for hsimport plugin --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 87 +++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index 0a5a663f6..bfdf77ec4 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -41,27 +41,35 @@ hsimportDescriptor plId = PluginDescriptor , pluginFormattingProvider = Nothing } +-- | Type of the symbol to import. +-- Important to offer the correct import list, or hiding code action. data SymbolType - = Symbol - | Constructor - | Type + = Symbol -- ^ Symbol is a simple function + | Constructor -- ^ Symbol is a constructor + | Type -- ^ Symbol is a type deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) -- | What of the symbol should be taken. +-- Import a simple symbol, or a value constructor. data SymbolKind - = Only SymbolName -- ^ only the symbol should be taken - | AllOf DatatypeName -- ^ all constructors or methods of the symbol should be taken: Symbol(..) - | OneOf DatatypeName SymbolName -- ^ some constructors or methods of the symbol should be taken: Symbol(X, Y) + = Only SymbolName -- ^ Only the symbol should be taken + | OneOf DatatypeName SymbolName -- ^ Some constructors or methods of the symbol should be taken: Symbol(X) + | AllOf DatatypeName -- ^ All constructors or methods of the symbol should be taken: Symbol(..) deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) --- | The imported or from the import hidden symbol. +-- | Disambiguates between an import action and an hiding action. +-- Can be used to determine suggestion tpye from ghc-mod, +-- e.g. whether ghc-mod suggests to hide an identifier or to import an identifier. +-- Also important later, to know how the symbol shall be imported. data SymbolImport a = Import a -- ^ the symbol to import | Hiding a -- ^ the symbol to hide from the import deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) +-- | Utility to retrieve the contents of the 'SymbolImport'. +-- May never fail. extractSymbolImport :: SymbolImport a -> a extractSymbolImport (Hiding s) = s extractSymbolImport (Import s) = s @@ -70,11 +78,19 @@ type ModuleName = T.Text type SymbolName = T.Text type DatatypeName = T.Text +-- | How to import a module. +-- Can be used to express to import a whole module or only specific symbols +-- from a module. +-- Is used to either hide symbols from an import or use an import-list to +-- import only a specific symbol. data ImportStyle = Simple -- ^ Import the whole module | Complex (SymbolImport SymbolKind) -- ^ Complex operation, import module hiding symbols or import only selected symbols. deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) +-- | Contains information about the diagnostic, the symbol ghc-mod +-- complained about and what the kind of the symbol is and whether +-- to import or hide the symbol as suggested by ghc-mod. data ImportDiagnostic = ImportDiagnostic { diagnostic :: J.Diagnostic , term :: SymbolName @@ -82,7 +98,6 @@ data ImportDiagnostic = ImportDiagnostic } deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) - -- | Import Parameters for Modules. -- Can be used to import every symbol from a module, -- or to import only a specific function from a module. @@ -189,23 +204,31 @@ importModule uri impStyle modName = $ IdeResultOk (J.WorkspaceEdit newChanges newDocChanges) else return $ IdeResultOk (J.WorkspaceEdit mChanges mDocChanges) +-- | Convert the import style arguments into HsImport arguments. +-- Takes an input and an output file as well as a module name. importStyleToHsImportArgs :: FilePath -> FilePath -> ModuleName -> ImportStyle -> HsImport.HsImportArgs importStyleToHsImportArgs input output modName style = - let defaultArgs = + let defaultArgs = -- Default args, must be set every time. HsImport.defaultArgs { HsImport.moduleName = T.unpack modName , HsImport.inputSrcFile = input , HsImport.outputSrcFile = output } + + kindToArgs :: SymbolKind -> HsImport.HsImportArgs kindToArgs kind = case kind of + -- Only import a single symbol e.g. Data.Text (isPrefixOf) Only sym -> defaultArgs { HsImport.symbolName = T.unpack sym } + -- Import a constructor e.g. Data.Mabye (Maybe(Just)) OneOf dt sym -> defaultArgs { HsImport.symbolName = T.unpack dt , HsImport.with = [T.unpack sym] } + -- Import all constructors e.g. Data.Maybe (Maybe(..)) AllOf dt -> defaultArgs { HsImport.symbolName = T.unpack dt , HsImport.all = True } in case style of + -- If the import style is simple, import thw whole module Simple -> defaultArgs Complex s -> case s of Hiding kind -> kindToArgs kind {- TODO: wait for hsimport version bump -} @@ -265,8 +288,8 @@ codeActionProvider plId docId _ context = do (x : _) -> "is:exact " <> x applySearchStyle (Relax relax) termName = relax termName - -- | Turn a search term with function name into Import Actions. - -- Function name may be of only the exact phrase to import. + -- | Turn a search term with function name into an Import Actions. + -- The function name may be of only the exact phrase to import. -- The resulting CodeAction's contain a general import of a module or -- uses an Import-List. -- @@ -282,6 +305,12 @@ codeActionProvider plId docId _ context = do termToActions style modules impDiagnostic = concat <$> mapM (importModuleAction style impDiagnostic) modules + -- | Creates various import actions for a module and the diagnostic. + -- Possible import actions depend on the type of the symbol to import. + -- It may be a 'Constructor', so the import actions need to be different + -- to a simple function symbol. + -- Thus, it may return zero, one or multiple import actions for a module. + -- List of import actions does contain no duplicates. importModuleAction :: SearchStyle -> ImportDiagnostic -> ModuleName -> IdeM [J.CodeAction] importModuleAction searchStyle impDiagnostic moduleName = @@ -289,13 +318,22 @@ codeActionProvider plId docId _ context = do where importListActions :: [IdeM (Maybe J.CodeAction)] importListActions = case searchStyle of + -- If the search has been relaxed by a custom function, + -- we cant know how much the search query has been altered + -- and how close the result terms are to the initial diagnostic. + -- Thus, we cant offer more specific imports. Relax _ -> [] _ -> catMaybes $ case extractSymbolImport $ termType impDiagnostic of + -- If the term to import is a simple symbol, such as a function, + -- import only this function Symbol -> [ mkImportAction moduleName impDiagnostic . Just . Only <$> symName (term impDiagnostic) ] + -- Constructors can be imported in two ways, either all + -- constructors of a type or only a subset. + -- We can only import a single constructor at a time though. Constructor -> [ mkImportAction moduleName impDiagnostic . Just . AllOf <$> datatypeName (term impDiagnostic) @@ -304,22 +342,43 @@ codeActionProvider plId docId _ context = do <$> datatypeName (term impDiagnostic) <*> symName (term impDiagnostic) ] + -- If we are looking for a type, import it as just a symbol Type -> [ mkImportAction moduleName impDiagnostic . Just . Only <$> symName (term impDiagnostic)] + -- | All code actions that may be available + -- Currently, omits all codeActions :: [IdeM (Maybe J.CodeAction)] codeActions = case termType impDiagnostic of - Hiding _ -> [] + Hiding _ -> [] {- If we are hiding an import, we can not import + a module hiding everything from it. -} Import _ -> [mkImportAction moduleName impDiagnostic Nothing] + -- ^ Simple import, import the whole module ++ importListActions + -- | Retrieve the function signature of a term such as + -- >>> signatureOf "take :: Int -> [a] -> [a]" + -- Just " Int -> [a] -> [a]" signatureOf :: T.Text -> Maybe T.Text signatureOf sig = do let parts = T.splitOn "::" sig typeSig <- S.tailMay parts S.headMay typeSig + -- | Retrieve the datatype name of a Constructor. + -- + -- >>> datatypeName "Null :: Data.Aeson.Internal.Types.Value" + -- Just "Value" + -- + -- >>> datatypeName "take :: Int -> [a] -> [a]" -- Not a constructor + -- Just "[a]" + -- + -- >>> datatypeName "Just :: a -> Maybe a" + -- Just "Maybe" + -- + -- Thus, the result of this function only makes sense, + -- if the symbol kind of the diagnostic term is of type 'Constructor' datatypeName :: T.Text -> Maybe T.Text datatypeName sig = do sig_ <- signatureOf sig @@ -330,6 +389,10 @@ codeActionProvider plId docId _ context = do let qualifiedDtNameParts = T.splitOn "." qualifiedDtName S.lastMay qualifiedDtNameParts + -- | Name of a symbol. May contain a function signature. + -- + -- >>> symName "take :: Int -> [a] -> [a]" + -- Just "take" symName :: T.Text -> Maybe SymbolName symName = S.headMay . T.words From 3835a09db92c3a3c4f0761e50cd22e70f191c603 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 17:55:33 +0200 Subject: [PATCH 03/12] Fix tests, add documentation and add newtypes --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 57 +++++++++++++++-------- test/unit/CodeActionsSpec.hs | 15 +++--- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index bfdf77ec4..8827bc7f8 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -78,6 +78,12 @@ type ModuleName = T.Text type SymbolName = T.Text type DatatypeName = T.Text +-- | Wrapper for a FilePath that is used as an Input file for HsImport +newtype InputFilePath = MkInputFilePath { getInput :: FilePath } + +-- | Wrapper for a FilePath that is used as an Output file for HsImport +newtype OutputFilePath = MkOutputFilePath { getOutput :: FilePath } + -- | How to import a module. -- Can be used to express to import a whole module or only specific symbols -- from a module. @@ -128,7 +134,11 @@ importModule uri impStyle modName = tmpDir <- liftIO getTemporaryDirectory (output, outputH) <- liftIO $ openTempFile tmpDir "hsimportOutput" liftIO $ hClose outputH - let args = importStyleToHsImportArgs input output modName impStyle + let args = importStyleToHsImportArgs + (MkInputFilePath input) + (MkOutputFilePath output) + modName + impStyle -- execute hsimport on the given file and write into a temporary file. maybeErr <- liftIO $ HsImport.hsimportWithArgs HsImport.defaultConfig args case maybeErr of @@ -207,12 +217,12 @@ importModule uri impStyle modName = -- | Convert the import style arguments into HsImport arguments. -- Takes an input and an output file as well as a module name. importStyleToHsImportArgs - :: FilePath -> FilePath -> ModuleName -> ImportStyle -> HsImport.HsImportArgs + :: InputFilePath -> OutputFilePath -> ModuleName -> ImportStyle -> HsImport.HsImportArgs importStyleToHsImportArgs input output modName style = let defaultArgs = -- Default args, must be set every time. HsImport.defaultArgs { HsImport.moduleName = T.unpack modName - , HsImport.inputSrcFile = input - , HsImport.outputSrcFile = output + , HsImport.inputSrcFile = getInput input + , HsImport.outputSrcFile = getOutput output } kindToArgs :: SymbolKind -> HsImport.HsImportArgs @@ -393,6 +403,9 @@ codeActionProvider plId docId _ context = do -- -- >>> symName "take :: Int -> [a] -> [a]" -- Just "take" + -- + -- >>> symName "take" + -- Just "take" symName :: T.Text -> Maybe SymbolName symName = S.headMay . T.words @@ -403,7 +416,7 @@ codeActionProvider plId docId _ context = do mkImportAction modName importDiagnostic symbolType = do cmd <- mkLspCommand plId "import" title (Just cmdParams) return (Just (codeAction cmd)) - where + where codeAction cmd = J.CodeAction title (Just J.CodeActionQuickFix) (Just (J.List [diagnostic importDiagnostic])) @@ -413,6 +426,8 @@ codeActionProvider plId docId _ context = do <> modName <> case termType importDiagnostic of Hiding _ -> "hiding " + -- ^ Note, that it must never happen + -- in combination with `symbolType == Nothing` Import _ -> "" <> case symbolType of Just s -> case s of @@ -442,25 +457,27 @@ codeActionProvider plId docId _ context = do -- This looks at the error message and tries to extract the expected -- signature of an unknown function. -- If this is not possible, Nothing is returned. -extractImportableTerm :: T.Text -> Maybe (T.Text, (SymbolImport SymbolType) ) +extractImportableTerm :: T.Text -> Maybe (T.Text, SymbolImport SymbolType) extractImportableTerm dirtyMsg = - let extractedTerm = - asum - [ (\name -> (name, Import Symbol)) <$> T.stripPrefix "Variable not in scope: " importMsg - , (\name -> (T.init name, Import Type)) <$> T.stripPrefix "Not in scope: type constructor or class ‘" importMsg - , (\name -> (name, Import Constructor)) <$> T.stripPrefix "Data constructor not in scope: " importMsg - ] + let extractedTerm = asum + [ (\name -> (name, Import Symbol)) + <$> T.stripPrefix "Variable not in scope: " importMsg + , (\name -> (T.init name, Import Type)) + <$> T.stripPrefix + "Not in scope: type constructor or class ‘" + importMsg + , (\name -> (name, Import Constructor)) + <$> T.stripPrefix "Data constructor not in scope: " importMsg] in do - (n, s) <- extractedTerm - let n' = T.strip n - return (n', s) - where - importMsg = - head - -- Get rid of the rename suggestion parts + (n, s) <- extractedTerm + let n' = T.strip n + return (n', s) + where + importMsg = head + -- Get rid of the rename suggestion parts $ T.splitOn "Perhaps you meant " $ T.replace "\n" " " - -- Get rid of trailing/leading whitespace on each individual line + -- Get rid of trailing/leading whitespace on each individual line $ T.unlines $ map T.strip $ T.lines diff --git a/test/unit/CodeActionsSpec.hs b/test/unit/CodeActionsSpec.hs index 2c5738cdb..73c2347eb 100644 --- a/test/unit/CodeActionsSpec.hs +++ b/test/unit/CodeActionsSpec.hs @@ -15,19 +15,22 @@ spec = do describe "import code actions" $ do it "pick up variable not in scope" $ let msg = "Variable not in scope: fromJust :: Maybe Integer -> t" - in extractImportableTerm msg `shouldBe` Just "fromJust :: Maybe Integer -> t" + in extractImportableTerm msg `shouldBe` Just ("fromJust :: Maybe Integer -> t", Import Symbol) it "pick up variable not in scope with 'perhaps you meant'" $ let msg = "• Variable not in scope: msgs :: T.Text\n• Perhaps you meant ‘msg’ (line 90)" - in extractImportableTerm msg `shouldBe` Just "msgs :: T.Text" + in extractImportableTerm msg `shouldBe` Just ("msgs :: T.Text", Import Symbol) it "pick up multi-line variable not in scope" $ let msg = "Variable not in scope:\nliftIO\n:: IO [FilePath]\n-> GhcMod.Monad.Newtypes.GmT\n (GhcMod.Monad.Newtypes.GmOutT IdeM) [[t0]]" - in extractImportableTerm msg `shouldBe` Just "liftIO :: IO [FilePath] -> GhcMod.Monad.Newtypes.GmT (GhcMod.Monad.Newtypes.GmOutT IdeM) [[t0]]" + in extractImportableTerm msg `shouldBe` Just ("liftIO :: IO [FilePath] -> GhcMod.Monad.Newtypes.GmT (GhcMod.Monad.Newtypes.GmOutT IdeM) [[t0]]", Import Symbol) it "pick up when" $ let msg = "Variable not in scope: when :: Bool -> IO () -> t" - in extractImportableTerm msg `shouldBe` Just "when :: Bool -> IO () -> t" + in extractImportableTerm msg `shouldBe` Just ("when :: Bool -> IO () -> t", Import Symbol) it "pick up data constructors" $ let msg = "Data constructor not in scope: ExitFailure :: Integer -> t" - in extractImportableTerm msg `shouldBe` Just "ExitFailure :: Integer -> t" + in extractImportableTerm msg `shouldBe` Just ("ExitFailure :: Integer -> t", Import Constructor) + it "pick up type" $ + let msg = "Not in scope: type constructor or class ‘Text" + in extractImportableTerm msg `shouldBe` Just ("Text", Import Type) describe "rename code actions" $ do it "pick up variable not in scope perhaps you meant" $ @@ -146,7 +149,7 @@ spec = do \ Text.Megaparsec.Error.ShowErrorComponent e, Ord t) =>\n\ \ OutputFormat -> Format.Result t e -> IO b" in extractMissingSignature msg `shouldBe` Just expected - + describe "unused term code actions" $ do it "pick up unused term" $ let msg = " Defined but not used: ‘imUnused’" From 25cb23801697b6a8f96c87c6d7d58194e421584e Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 19:09:33 +0200 Subject: [PATCH 04/12] Use correct datatype --- src/Haskell/Ide/Engine/Plugin/Hoogle.hs | 14 +++++++++++++- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 20 ++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs index de8401cc5..81d356897 100644 --- a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs +++ b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs @@ -152,7 +152,19 @@ renderTarget t = T.intercalate "\n" $ -- If an error occurs, such as no hoogle database has been found, -- or the search term has no match, an empty list will be returned. searchModules :: T.Text -> IdeM [T.Text] -searchModules = fmap (nub . take 5) . searchTargets (fmap (T.pack . fst) . targetModule) +searchModules = fmap (map fst) . searchModules' + +-- | Just like 'searchModules', but includes the signature of the search term +-- that has been found in the module. +searchModules' :: T.Text -> IdeM [(T.Text, T.Text)] +searchModules' = fmap (nub . take 5) + . searchTargets + (\target + -> (\modTarget -> (T.pack $ fst modTarget, normaliseItem . T.pack $ targetItem target)) + <$> targetModule target) + where + normaliseItem :: T.Text -> T.Text + normaliseItem = innerText . parseTags -- | Search for packages that satisfy the given search text. -- Will return at most five, unique results. diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index 8827bc7f8..b2f901e51 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -283,7 +283,7 @@ codeActionProvider plId docId _ context = do :: SearchStyle -> [ImportDiagnostic] -> IdeM [J.CodeAction] importActionsForTerms style importDiagnostics = do let searchTerms = map (applySearchStyle style . term) importDiagnostics - searchResults <- mapM Hoogle.searchModules searchTerms + searchResults <- mapM Hoogle.searchModules' searchTerms let importTerms = zip searchResults importDiagnostics concat <$> mapM (uncurry (termToActions style)) importTerms @@ -311,9 +311,9 @@ codeActionProvider plId docId _ context = do -- no import list can be offered, since the function name -- may be not the one we expect. termToActions - :: SearchStyle -> [ModuleName] -> ImportDiagnostic -> IdeM [J.CodeAction] + :: SearchStyle -> [(ModuleName, SymbolName)] -> ImportDiagnostic -> IdeM [J.CodeAction] termToActions style modules impDiagnostic = - concat <$> mapM (importModuleAction style impDiagnostic) modules + concat <$> mapM (uncurry (importModuleAction style impDiagnostic)) modules -- | Creates various import actions for a module and the diagnostic. -- Possible import actions depend on the type of the symbol to import. @@ -322,8 +322,8 @@ codeActionProvider plId docId _ context = do -- Thus, it may return zero, one or multiple import actions for a module. -- List of import actions does contain no duplicates. importModuleAction - :: SearchStyle -> ImportDiagnostic -> ModuleName -> IdeM [J.CodeAction] - importModuleAction searchStyle impDiagnostic moduleName = + :: SearchStyle -> ImportDiagnostic -> ModuleName -> SymbolName -> IdeM [J.CodeAction] + importModuleAction searchStyle impDiagnostic moduleName symbolTerm = catMaybes <$> sequenceA codeActions where importListActions :: [IdeM (Maybe J.CodeAction)] @@ -339,23 +339,23 @@ codeActionProvider plId docId _ context = do -- import only this function Symbol -> [ mkImportAction moduleName impDiagnostic . Just . Only - <$> symName (term impDiagnostic) + <$> symName symbolTerm ] -- Constructors can be imported in two ways, either all -- constructors of a type or only a subset. -- We can only import a single constructor at a time though. Constructor -> [ mkImportAction moduleName impDiagnostic . Just . AllOf - <$> datatypeName (term impDiagnostic) + <$> datatypeName symbolTerm , (\dt sym -> mkImportAction moduleName impDiagnostic . Just $ OneOf dt sym) - <$> datatypeName (term impDiagnostic) - <*> symName (term impDiagnostic) + <$> datatypeName symbolTerm + <*> symName symbolTerm ] -- If we are looking for a type, import it as just a symbol Type -> [ mkImportAction moduleName impDiagnostic . Just . Only - <$> symName (term impDiagnostic)] + <$> symName symbolTerm] -- | All code actions that may be available -- Currently, omits all From 3fe7f872cd1938c6d53404e969f32129dba3771c Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 19:44:40 +0200 Subject: [PATCH 05/12] Remove parenthesis for hsimport --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index b2f901e51..04edcb508 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -225,16 +225,28 @@ importStyleToHsImportArgs input output modName style = , HsImport.outputSrcFile = getOutput output } + -- | Remove parenthesis for operators and infix operator cosntructors. + -- HsImport demands it. E.g. + -- > hsimport -m Data.Array.Repa -s :. -w :. + -- import Data.Array.Repa ((:.)((:.))) + -- + -- > hsimport -m Data.Function -s $ + -- import Data.Function (($)) + trimParenthesis :: T.Text -> T.Text + trimParenthesis = T.dropAround isParenthesis + + isParenthesis = (`elem` ['(', ')']) + kindToArgs :: SymbolKind -> HsImport.HsImportArgs kindToArgs kind = case kind of -- Only import a single symbol e.g. Data.Text (isPrefixOf) - Only sym -> defaultArgs { HsImport.symbolName = T.unpack sym } + Only sym -> defaultArgs { HsImport.symbolName = T.unpack $ trimParenthesis sym } -- Import a constructor e.g. Data.Mabye (Maybe(Just)) - OneOf dt sym -> defaultArgs { HsImport.symbolName = T.unpack dt - , HsImport.with = [T.unpack sym] + OneOf dt sym -> defaultArgs { HsImport.symbolName = T.unpack $ trimParenthesis dt + , HsImport.with = [T.unpack $ trimParenthesis sym] } -- Import all constructors e.g. Data.Maybe (Maybe(..)) - AllOf dt -> defaultArgs { HsImport.symbolName = T.unpack dt + AllOf dt -> defaultArgs { HsImport.symbolName = T.unpack $ trimParenthesis dt , HsImport.all = True } in case style of From a1f0bed59dc78c70dc343e11f57bd53d5ae16c94 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 19:45:00 +0200 Subject: [PATCH 06/12] Add testdata file --- test/testdata/CodeActionImportListElaborate.hs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test/testdata/CodeActionImportListElaborate.hs diff --git a/test/testdata/CodeActionImportListElaborate.hs b/test/testdata/CodeActionImportListElaborate.hs new file mode 100644 index 000000000..1cf149fd2 --- /dev/null +++ b/test/testdata/CodeActionImportListElaborate.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE NoImplicitPrelude #-} +import System.IO (IO) +-- | Main entry point to the program +main :: IO () +main = + when True + $ hPutStrLn stdout + $ fromMaybe "Good night, World!" (Just "Hello, World!") \ No newline at end of file From 988dcff2eb18edcee0bbed5a10e597f9ca9d1ad9 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 19:47:26 +0200 Subject: [PATCH 07/12] Add space to code action title --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index 04edcb508..ec0026496 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -440,7 +440,7 @@ codeActionProvider plId docId _ context = do Hiding _ -> "hiding " -- ^ Note, that it must never happen -- in combination with `symbolType == Nothing` - Import _ -> "" + Import _ -> " " <> case symbolType of Just s -> case s of Only sym -> "(" <> sym <> ")" From bcac2b52cbcbf3fe4b03ccdb866e830c8f384cc9 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 20:04:10 +0200 Subject: [PATCH 08/12] Implement tests for importing constructors --- test/functional/FunctionalCodeActionsSpec.hs | 94 +++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/test/functional/FunctionalCodeActionsSpec.hs b/test/functional/FunctionalCodeActionsSpec.hs index 07ab9d6ff..ca2df3d10 100644 --- a/test/functional/FunctionalCodeActionsSpec.hs +++ b/test/functional/FunctionalCodeActionsSpec.hs @@ -153,6 +153,25 @@ spec = describe "code actions" $ do , " $ hPutStrLn stdout" , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" ] + , -- Complex imports for Constructos and functions + [ "{-# LANGUAGE NoImplicitPrelude #-}" + , "import System.IO ( IO" + , " , hPutStrLn" + , " , stdout" + , " )" + , "import Prelude ( Bool(..) )" + , "import Control.Monad ( when )" + , "import Data.Maybe ( fromMaybe" + , " , Maybe(Just)" + , " )" + , "import Data.Function ( ($) )" + , "-- | Main entry point to the program" + , "main :: IO ()" + , "main =" + , " when True" + , " $ hPutStrLn stdout" + , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" + ] ] hsImportSpec "floskell" [ -- Expected output for simple format. @@ -178,6 +197,20 @@ spec = describe "code actions" $ do , " $ hPutStrLn stdout" , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" ] + , -- Complex imports for Constructos and functions + [ "{-# LANGUAGE NoImplicitPrelude #-}" + , "import System.IO (IO, hPutStrLn, stdout)" + , "import Prelude (Bool(..))" + , "import Control.Monad (when)" + , "import Data.Maybe (fromMaybe, Maybe(Just))" + , "import Data.Function (($))" + , "-- | Main entry point to the program" + , "main :: IO ()" + , "main =" + , " when True" + , " $ hPutStrLn stdout" + , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" + ] ] describe "add package suggestions" $ do -- Only execute this test with ghc 8.4.4, below seems to be broken in the package. @@ -504,7 +537,7 @@ spec = describe "code actions" $ do -- Parameterized HsImport Spec. -- --------------------------------------------------------------------- hsImportSpec :: T.Text -> [[T.Text]]-> Spec -hsImportSpec formatterName [e1, e2, e3] = +hsImportSpec formatterName [e1, e2, e3, e4] = describe ("Execute HsImport with formatter " <> T.unpack formatterName) $ do it "works with 3.8 code action kinds" $ runSession hieCommand fullCaps "test/testdata" $ do doc <- openDoc "CodeActionImport.hs" "haskell" @@ -626,7 +659,7 @@ hsImportSpec formatterName [e1, e2, e3] = l3 `shouldBe` "main :: IO ()" l4 `shouldBe` "main = when True $ putStrLn \"hello\"" - it ("import-list respects format config with " <> T.unpack formatterName) $ runSession hieCommand fullCaps "test/testdata" $ do + it "import-list respects format config" $ runSession hieCommand fullCaps "test/testdata" $ do doc <- openDoc "CodeActionImportBrittany.hs" "haskell" _ <- waitForDiagnosticsSource "ghcmod" @@ -644,6 +677,63 @@ hsImportSpec formatterName [e1, e2, e3] = l2 `shouldBe` "import Control.Monad (when)" l3 `shouldBe` "main :: IO ()" l4 `shouldBe` "main = when True $ putStrLn \"hello\"" + + it "complex import-list" $ runSession hieCommand fullCaps "test/testdata" $ do + doc <- openDoc "CodeActionImportListElaborate.hs" "haskell" + _ <- waitForDiagnosticsSource "ghcmod" + + let config = def { formatOnImportOn = False, formattingProvider = formatterName } + sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams (toJSON config)) + + let wantedCodeActionTitles = [ "Import module System.IO (hPutSetrLn)" + , "Import module System.IO (stdout)" + , "Import module Control.Monad (when)" + , "Import module Data.Maybe (fromMaybe)" + , "Import module Data.Function (($))" + , "Import module Data.Maybe (Maybe(Just))" + , "Import module Prelude (Bool(..))" + ] + + executeAllCodeActions doc wantedCodeActionTitles + + contents <- getDocumentEdit doc + liftIO $ + T.lines contents `shouldBe` e4 + + it "complex import-list respects format config" $ runSession hieCommand fullCaps "test/testdata" $ do + doc <- openDoc "CodeActionImportListElaborate.hs" "haskell" + _ <- waitForDiagnosticsSource "ghcmod" + + let config = def { formatOnImportOn = False, formattingProvider = formatterName } + sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams (toJSON config)) + + let wantedCodeActionTitles = [ "Import module System.IO (hPutStrLn)" + , "Import module System.IO (stdout)" + , "Import module Control.Monad (when)" + , "Import module Data.Maybe (fromMaybe)" + , "Import module Data.Function (($))" + , "Import module Data.Maybe (Maybe(Just))" + , "Import module Prelude (Bool(..))" + ] + + executeAllCodeActions doc wantedCodeActionTitles + + contents <- getDocumentEdit doc + liftIO $ do + let [l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12] = T.lines contents + l1 `shouldBe` "{-# LANGUAGE NoImplicitPrelude #-}" + l2 `shouldBe` "import System.IO (IO, hPutStrLn, stdout)" + l3 `shouldBe` "import Prelude (Bool(..))" + l4 `shouldBe` "import Control.Monad (when)" + l5 `shouldBe` "import Data.Maybe (fromMaybe, Maybe(Just))" + l6 `shouldBe` "import Data.Function (($))" + l7 `shouldBe` "-- | Main entry point to the program" + l8 `shouldBe` "main :: IO ()" + l9 `shouldBe` "main =" + l10 `shouldBe` " when True" + l11 `shouldBe` " $ hPutStrLn stdout" + l12 `shouldBe` " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" + where executeAllCodeActions :: TextDocumentIdentifier -> [T.Text] -> Session () executeAllCodeActions doc names = From 8e24b1293f8fe80961cb2bcf44b3b7dd1ecb3db9 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 4 Jun 2019 20:59:09 +0200 Subject: [PATCH 09/12] Add spaces to the correct positions --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 10 +++---- test/functional/FunctionalCodeActionsSpec.hs | 28 +++++++++---------- .../testdata/CodeActionImportListElaborate.hs | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index ec0026496..3ead8eefd 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -437,15 +437,15 @@ codeActionProvider plId docId _ context = do title = "Import module " <> modName <> case termType importDiagnostic of - Hiding _ -> "hiding " + Hiding _ -> "hiding" -- ^ Note, that it must never happen -- in combination with `symbolType == Nothing` - Import _ -> " " + Import _ -> "" <> case symbolType of Just s -> case s of - Only sym -> "(" <> sym <> ")" - AllOf dt -> "(" <> dt <> " (..))" - OneOf dt sym -> "(" <> dt <> " (" <> sym <> "))" + Only sym -> " (" <> sym <> ")" + AllOf dt -> " (" <> dt <> " (..))" + OneOf dt sym -> " (" <> dt <> " (" <> sym <> "))" Nothing -> "" importStyleParam :: ImportStyle diff --git a/test/functional/FunctionalCodeActionsSpec.hs b/test/functional/FunctionalCodeActionsSpec.hs index ca2df3d10..0c0fa5103 100644 --- a/test/functional/FunctionalCodeActionsSpec.hs +++ b/test/functional/FunctionalCodeActionsSpec.hs @@ -157,7 +157,7 @@ spec = describe "code actions" $ do [ "{-# LANGUAGE NoImplicitPrelude #-}" , "import System.IO ( IO" , " , hPutStrLn" - , " , stdout" + , " , stderr" , " )" , "import Prelude ( Bool(..) )" , "import Control.Monad ( when )" @@ -169,7 +169,7 @@ spec = describe "code actions" $ do , "main :: IO ()" , "main =" , " when True" - , " $ hPutStrLn stdout" + , " $ hPutStrLn stderr" , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" ] ] @@ -199,7 +199,7 @@ spec = describe "code actions" $ do ] , -- Complex imports for Constructos and functions [ "{-# LANGUAGE NoImplicitPrelude #-}" - , "import System.IO (IO, hPutStrLn, stdout)" + , "import System.IO (IO, hPutStrLn, stderr)" , "import Prelude (Bool(..))" , "import Control.Monad (when)" , "import Data.Maybe (fromMaybe, Maybe(Just))" @@ -208,7 +208,7 @@ spec = describe "code actions" $ do , "main :: IO ()" , "main =" , " when True" - , " $ hPutStrLn stdout" + , " $ hPutStrLn stderr" , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" ] ] @@ -685,13 +685,13 @@ hsImportSpec formatterName [e1, e2, e3, e4] = let config = def { formatOnImportOn = False, formattingProvider = formatterName } sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams (toJSON config)) - let wantedCodeActionTitles = [ "Import module System.IO (hPutSetrLn)" - , "Import module System.IO (stdout)" + let wantedCodeActionTitles = [ "Import module System.IO (hPutStrLn)" , "Import module Control.Monad (when)" , "Import module Data.Maybe (fromMaybe)" , "Import module Data.Function (($))" - , "Import module Data.Maybe (Maybe(Just))" - , "Import module Prelude (Bool(..))" + , "Import module Data.Maybe (Maybe (Just))" + , "Import module Prelude (Bool (..))" + , "Import module System.IO (stderr)" ] executeAllCodeActions doc wantedCodeActionTitles @@ -708,12 +708,12 @@ hsImportSpec formatterName [e1, e2, e3, e4] = sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams (toJSON config)) let wantedCodeActionTitles = [ "Import module System.IO (hPutStrLn)" - , "Import module System.IO (stdout)" , "Import module Control.Monad (when)" , "Import module Data.Maybe (fromMaybe)" , "Import module Data.Function (($))" - , "Import module Data.Maybe (Maybe(Just))" - , "Import module Prelude (Bool(..))" + , "Import module Data.Maybe (Maybe (Just))" + , "Import module Prelude (Bool (..))" + , "Import module System.IO (stderr)" ] executeAllCodeActions doc wantedCodeActionTitles @@ -722,7 +722,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = liftIO $ do let [l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12] = T.lines contents l1 `shouldBe` "{-# LANGUAGE NoImplicitPrelude #-}" - l2 `shouldBe` "import System.IO (IO, hPutStrLn, stdout)" + l2 `shouldBe` "import System.IO (IO, hPutStrLn, stderr)" l3 `shouldBe` "import Prelude (Bool(..))" l4 `shouldBe` "import Control.Monad (when)" l5 `shouldBe` "import Data.Maybe (fromMaybe, Maybe(Just))" @@ -731,7 +731,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = l8 `shouldBe` "main :: IO ()" l9 `shouldBe` "main =" l10 `shouldBe` " when True" - l11 `shouldBe` " $ hPutStrLn stdout" + l11 `shouldBe` " $ hPutStrLn stderr" l12 `shouldBe` " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" where @@ -754,7 +754,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = error $ "Found an unexpected amount of action. Expected 1, but got: " ++ show (length xs) - ++ "\n. Titles: " ++ show (map (^. L.title) allActions) + ++ ".\n Titles: " ++ show (map (^. L.title) allActions) -- Silence warnings hsImportSpec formatter args = diff --git a/test/testdata/CodeActionImportListElaborate.hs b/test/testdata/CodeActionImportListElaborate.hs index 1cf149fd2..587f9c042 100644 --- a/test/testdata/CodeActionImportListElaborate.hs +++ b/test/testdata/CodeActionImportListElaborate.hs @@ -4,5 +4,5 @@ import System.IO (IO) main :: IO () main = when True - $ hPutStrLn stdout + $ hPutStrLn stderr $ fromMaybe "Good night, World!" (Just "Hello, World!") \ No newline at end of file From 1c573a07d25e4fc3cb7198e8821df863a8f6f360 Mon Sep 17 00:00:00 2001 From: fendor Date: Sat, 8 Jun 2019 15:22:18 +0200 Subject: [PATCH 10/12] Implement suggestions --- src/Haskell/Ide/Engine/Plugin/Hoogle.hs | 29 +++++++++++++------- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 32 +++++++++++------------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs index 81d356897..eea4e2fca 100644 --- a/src/Haskell/Ide/Engine/Plugin/Hoogle.hs +++ b/src/Haskell/Ide/Engine/Plugin/Hoogle.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE TupleSections #-} {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Haskell.Ide.Engine.Plugin.Hoogle where @@ -5,6 +6,7 @@ module Haskell.Ide.Engine.Plugin.Hoogle where import Control.Monad.IO.Class import Control.Monad (join) import Control.Exception +import Control.Applicative (liftA2) import Data.Aeson import Data.Bifunctor import Data.Maybe @@ -157,14 +159,23 @@ searchModules = fmap (map fst) . searchModules' -- | Just like 'searchModules', but includes the signature of the search term -- that has been found in the module. searchModules' :: T.Text -> IdeM [(T.Text, T.Text)] -searchModules' = fmap (nub . take 5) - . searchTargets - (\target - -> (\modTarget -> (T.pack $ fst modTarget, normaliseItem . T.pack $ targetItem target)) - <$> targetModule target) - where - normaliseItem :: T.Text -> T.Text - normaliseItem = innerText . parseTags +searchModules' = fmap (take 5 . nub) . searchTargets retrieveModuleAndSignature + where + -- | Hoogle results contain html like tags. + -- We remove them with `tagsoup` here. + -- So, if something hoogle related shows html tags, + -- then maybe this function is responsible. + normaliseItem :: T.Text -> T.Text + normaliseItem = innerText . parseTags + + retrieveModuleAndSignature :: Target -> Maybe (T.Text, T.Text) + retrieveModuleAndSignature target = liftA2 (,) (packModuleName target) (packSymbolSignature target) + + packModuleName :: Target -> Maybe T.Text + packModuleName = fmap (T.pack . fst) . targetModule + + packSymbolSignature :: Target -> Maybe T.Text + packSymbolSignature = Just . normaliseItem . T.pack . targetItem -- | Search for packages that satisfy the given search text. -- Will return at most five, unique results. @@ -172,7 +183,7 @@ searchModules' = fmap (nub . take 5) -- If an error occurs, such as no hoogle database has been found, -- or the search term has no match, an empty list will be returned. searchPackages :: T.Text -> IdeM [T.Text] -searchPackages = fmap (nub . take 5) . searchTargets (fmap (T.pack . fst) . targetPackage) +searchPackages = fmap (take 5 . nub) . searchTargets (fmap (T.pack . fst) . targetPackage) -- | Search for Targets that fit to the given Text and satisfy the given predicate. -- Limits the amount of matches to at most ten. diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index 3ead8eefd..f19bcfaa9 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -470,27 +470,27 @@ codeActionProvider plId docId _ context = do -- signature of an unknown function. -- If this is not possible, Nothing is returned. extractImportableTerm :: T.Text -> Maybe (T.Text, SymbolImport SymbolType) -extractImportableTerm dirtyMsg = - let extractedTerm = asum - [ (\name -> (name, Import Symbol)) - <$> T.stripPrefix "Variable not in scope: " importMsg - , (\name -> (T.init name, Import Type)) - <$> T.stripPrefix - "Not in scope: type constructor or class ‘" - importMsg - , (\name -> (name, Import Constructor)) - <$> T.stripPrefix "Data constructor not in scope: " importMsg] - in do - (n, s) <- extractedTerm - let n' = T.strip n - return (n', s) +extractImportableTerm dirtyMsg = do + (n, s) <- extractedTerm + let n' = T.strip n + return (n', s) where importMsg = head - -- Get rid of the rename suggestion parts + -- Get rid of the rename suggestion parts $ T.splitOn "Perhaps you meant " $ T.replace "\n" " " - -- Get rid of trailing/leading whitespace on each individual line + -- Get rid of trailing/leading whitespace on each individual line $ T.unlines $ map T.strip $ T.lines $ T.replace "• " "" dirtyMsg + + extractedTerm = asum + [ (\name -> (name, Import Symbol)) + <$> T.stripPrefix "Variable not in scope: " importMsg + , (\name -> (T.init name, Import Type)) + <$> T.stripPrefix + "Not in scope: type constructor or class ‘" + importMsg + , (\name -> (name, Import Constructor)) + <$> T.stripPrefix "Data constructor not in scope: " importMsg] From 1a59411c7688ceab2163f855cf22a5a94216e2a2 Mon Sep 17 00:00:00 2001 From: fendor Date: Sat, 8 Jun 2019 16:22:08 +0200 Subject: [PATCH 11/12] Fix hsimport tests --- test/functional/FunctionalCodeActionsSpec.hs | 87 ++++++++++---------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/test/functional/FunctionalCodeActionsSpec.hs b/test/functional/FunctionalCodeActionsSpec.hs index 0c0fa5103..1bd5d3747 100644 --- a/test/functional/FunctionalCodeActionsSpec.hs +++ b/test/functional/FunctionalCodeActionsSpec.hs @@ -127,7 +127,7 @@ spec = describe "code actions" $ do liftIO $ x `shouldBe` "foo = putStrLn \"world\"" describe "import suggestions" $ do - hsImportSpec "brittany" + describe "formats with brittany" $ hsImportSpec "brittany" [ -- Expected output for simple format. [ "import qualified Data.Maybe" , "import Control.Monad" @@ -155,16 +155,16 @@ spec = describe "code actions" $ do ] , -- Complex imports for Constructos and functions [ "{-# LANGUAGE NoImplicitPrelude #-}" - , "import System.IO ( IO" - , " , hPutStrLn" - , " , stderr" - , " )" - , "import Prelude ( Bool(..) )" - , "import Control.Monad ( when )" - , "import Data.Maybe ( fromMaybe" - , " , Maybe(Just)" - , " )" - , "import Data.Function ( ($) )" + , "import System.IO ( IO" + , " , hPutStrLn" + , " , stderr" + , " )" + , "import Prelude ( Bool(..) )" + , "import Control.Monad ( when )" + , "import Data.Function ( ($) )" + , "import Data.Maybe ( fromMaybe" + , " , Maybe(Just)" + , " )" , "-- | Main entry point to the program" , "main :: IO ()" , "main =" @@ -173,7 +173,7 @@ spec = describe "code actions" $ do , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" ] ] - hsImportSpec "floskell" + describe "formats with floskell" $ hsImportSpec "floskell" [ -- Expected output for simple format. [ "import qualified Data.Maybe" , "import Control.Monad" @@ -202,8 +202,8 @@ spec = describe "code actions" $ do , "import System.IO (IO, hPutStrLn, stderr)" , "import Prelude (Bool(..))" , "import Control.Monad (when)" - , "import Data.Maybe (fromMaybe, Maybe(Just))" , "import Data.Function (($))" + , "import Data.Maybe (fromMaybe, Maybe(Just))" , "-- | Main entry point to the program" , "main :: IO ()" , "main =" @@ -609,9 +609,8 @@ hsImportSpec formatterName [e1, e2, e3, e4] = , "Import module Data.Maybe (fromMaybe)" ] - executeAllCodeActions doc wantedCodeActionTitles + contents <- executeAllCodeActions doc wantedCodeActionTitles - contents <- documentContents doc liftIO $ Set.fromList (T.lines contents) `shouldBe` Set.fromList e3 it "respects format config, multiple import-list" $ runSession hieCommand fullCaps "test/testdata" $ do @@ -626,8 +625,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = , "Import module Data.Maybe (fromMaybe)" ] - executeAllCodeActions doc wantedCodeActionTitles - contents <- documentContents doc + contents <- executeAllCodeActions doc wantedCodeActionTitles liftIO $ Set.fromList (T.lines contents) `shouldBe` Set.fromList [ "import System.IO (stdout, hPutStrLn)" @@ -682,7 +680,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = doc <- openDoc "CodeActionImportListElaborate.hs" "haskell" _ <- waitForDiagnosticsSource "ghcmod" - let config = def { formatOnImportOn = False, formattingProvider = formatterName } + let config = def { formatOnImportOn = True, formattingProvider = formatterName } sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams (toJSON config)) let wantedCodeActionTitles = [ "Import module System.IO (hPutStrLn)" @@ -694,9 +692,8 @@ hsImportSpec formatterName [e1, e2, e3, e4] = , "Import module System.IO (stderr)" ] - executeAllCodeActions doc wantedCodeActionTitles + contents <- executeAllCodeActions doc wantedCodeActionTitles - contents <- getDocumentEdit doc liftIO $ T.lines contents `shouldBe` e4 @@ -716,32 +713,36 @@ hsImportSpec formatterName [e1, e2, e3, e4] = , "Import module System.IO (stderr)" ] - executeAllCodeActions doc wantedCodeActionTitles + contents <- executeAllCodeActions doc wantedCodeActionTitles - contents <- getDocumentEdit doc - liftIO $ do - let [l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12] = T.lines contents - l1 `shouldBe` "{-# LANGUAGE NoImplicitPrelude #-}" - l2 `shouldBe` "import System.IO (IO, hPutStrLn, stderr)" - l3 `shouldBe` "import Prelude (Bool(..))" - l4 `shouldBe` "import Control.Monad (when)" - l5 `shouldBe` "import Data.Maybe (fromMaybe, Maybe(Just))" - l6 `shouldBe` "import Data.Function (($))" - l7 `shouldBe` "-- | Main entry point to the program" - l8 `shouldBe` "main :: IO ()" - l9 `shouldBe` "main =" - l10 `shouldBe` " when True" - l11 `shouldBe` " $ hPutStrLn stderr" - l12 `shouldBe` " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" + liftIO $ + T.lines contents `shouldBe` + [ "{-# LANGUAGE NoImplicitPrelude #-}" + , "import System.IO (IO, hPutStrLn, stderr)" + , "import Prelude (Bool(..))" + , "import Control.Monad (when)" + , "import Data.Function (($))" + , "import Data.Maybe (fromMaybe, Maybe(Just))" + , "-- | Main entry point to the program" + , "main :: IO ()" + , "main =" + , " when True" + , " $ hPutStrLn stderr" + , " $ fromMaybe \"Good night, World!\" (Just \"Hello, World!\")" + ] where - executeAllCodeActions :: TextDocumentIdentifier -> [T.Text] -> Session () + executeAllCodeActions :: TextDocumentIdentifier -> [T.Text] -> Session T.Text executeAllCodeActions doc names = - replicateM_ (length names) $ do - _ <- waitForDiagnosticsSource "ghcmod" - executeCodeActionByName doc names - _ <- skipManyTill publishDiagnosticsNotification $ getDocumentEdit doc - waitForDiagnosticsSource "ghcmod" + foldM (\_ _ -> do + _ <- waitForDiagnosticsSource "ghcmod" + executeCodeActionByName doc names + content <- skipManyTill publishDiagnosticsNotification $ getDocumentEdit doc + _ <- waitForDiagnosticsSource "ghcmod" + return content + ) + (T.pack "") + [ 1 .. length names ] executeCodeActionByName :: TextDocumentIdentifier -> [T.Text] -> Session () executeCodeActionByName doc names = do @@ -760,7 +761,7 @@ hsImportSpec formatterName [e1, e2, e3, e4] = hsImportSpec formatter args = error $ "Not the right amount of arguments for \"hsImportSpec (" ++ T.unpack formatter - ++ ")\", expected 3, got " + ++ ")\", expected 4, got " ++ show (length args) -- --------------------------------------------------------------------- From 40918950533e8138d3424019175def7121f88617 Mon Sep 17 00:00:00 2001 From: fendor Date: Wed, 12 Jun 2019 22:24:21 +0200 Subject: [PATCH 12/12] Use total function --- src/Haskell/Ide/Engine/Plugin/HsImport.hs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Haskell/Ide/Engine/Plugin/HsImport.hs b/src/Haskell/Ide/Engine/Plugin/HsImport.hs index f19bcfaa9..ea28eef27 100644 --- a/src/Haskell/Ide/Engine/Plugin/HsImport.hs +++ b/src/Haskell/Ide/Engine/Plugin/HsImport.hs @@ -475,7 +475,7 @@ extractImportableTerm dirtyMsg = do let n' = T.strip n return (n', s) where - importMsg = head + importMsg = S.headMay -- Get rid of the rename suggestion parts $ T.splitOn "Perhaps you meant " $ T.replace "\n" " " @@ -486,11 +486,14 @@ extractImportableTerm dirtyMsg = do $ T.replace "• " "" dirtyMsg extractedTerm = asum - [ (\name -> (name, Import Symbol)) - <$> T.stripPrefix "Variable not in scope: " importMsg - , (\name -> (T.init name, Import Type)) - <$> T.stripPrefix - "Not in scope: type constructor or class ‘" - importMsg - , (\name -> (name, Import Constructor)) - <$> T.stripPrefix "Data constructor not in scope: " importMsg] + [ importMsg + >>= T.stripPrefix "Variable not in scope: " + >>= \name -> Just (name, Import Symbol) + , importMsg + >>= T.stripPrefix "Not in scope: type constructor or class ‘" + >>= \name -> Just (T.init name, Import Type) + , importMsg + >>= T.stripPrefix "Data constructor not in scope: " + >>= \name -> Just (name, Import Constructor)] + +