From 491723ea1c0516575600c46ae3841d3884c2583c Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Sun, 31 Jan 2021 21:26:26 +0000 Subject: [PATCH 1/9] Implement refine imports --- cabal.project | 1 + exe/Plugins.hs | 6 +- haskell-language-server.cabal | 1 + plugins/hls-refine-imports-plugin/LICENSE | 201 ++++++++++++++++ .../hls-refine-imports-plugin.cabal | 27 +++ .../src/Ide/Plugin/RefineImports.hs | 223 ++++++++++++++++++ stack-8.8.4.yaml | 1 + 7 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 plugins/hls-refine-imports-plugin/LICENSE create mode 100644 plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal create mode 100644 plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs diff --git a/cabal.project b/cabal.project index 745f326e6e..88546ddb78 100644 --- a/cabal.project +++ b/cabal.project @@ -11,6 +11,7 @@ packages: ./plugins/hls-class-plugin ./plugins/hls-eval-plugin ./plugins/hls-explicit-imports-plugin + ./plugins/hls-refine-imports-plugin ./plugins/hls-hlint-plugin ./plugins/hls-retrie-plugin ./plugins/hls-haddock-comments-plugin diff --git a/exe/Plugins.hs b/exe/Plugins.hs index d6c37789fa..500509cd6b 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -26,7 +26,8 @@ import Ide.Plugin.Eval as Eval #endif #if importLens -import Ide.Plugin.ExplicitImports as ExplicitImports +-- import Ide.Plugin.ExplicitImports as ExplicitImports +import Ide.Plugin.RefineImports as RefineImports #endif #if retrie @@ -123,7 +124,8 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins Eval.descriptor "eval" : #endif #if importLens - ExplicitImports.descriptor "importLens" : + -- , ExplicitImports.descriptor "importLens" + , RefineImports.descriptor "refineImports" #endif #if moduleName ModuleName.descriptor "moduleName" : diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 43a277118b..0c640d9425 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -203,6 +203,7 @@ common eval common importLens if flag(importLens) || flag(all-plugins) build-depends: hls-explicit-imports-plugin ^>= 1.0.0.0 + hls-refine-imports-plugin cpp-options: -DimportLens common retrie diff --git a/plugins/hls-refine-imports-plugin/LICENSE b/plugins/hls-refine-imports-plugin/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/hls-refine-imports-plugin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal new file mode 100644 index 0000000000..3e8508a682 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal @@ -0,0 +1,27 @@ +cabal-version: 2.2 +name: hls-refine-imports-plugin +version: 0.1.0.0 +synopsis: Refine imports plugin for Haskell Language Server +license: Apache-2.0 +license-file: LICENSE +author: rayshih +maintainer: mnf.shih@gmail.com +category: Development +build-type: Simple + +library + exposed-modules: Ide.Plugin.RefineImports + hs-source-dirs: src + build-depends: aeson + , base >=4.12 && <5 + , containers + , deepseq + , haskell-lsp-types + , hls-plugin-api + , ghc + , ghcide + , shake + , text + , unordered-containers + + default-language: Haskell2010 \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs new file mode 100644 index 0000000000..775829405d --- /dev/null +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -0,0 +1,223 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DuplicateRecordFields #-} +module Ide.Plugin.RefineImports (descriptor) where + +import Development.IDE +import Ide.Types +import Language.Haskell.LSP.Types +import Development.Shake.Classes +import GHC.Generics (Generic) +import Development.IDE.GHC.Compat +import qualified Data.Map.Strict as Map +import qualified Data.Text as T +import Data.Maybe (fromMaybe, catMaybes) +import Control.Monad.IO.Class (liftIO) +import Control.DeepSeq (rwhnf) +import TcRnMonad (tcg_used_gres, initTcWithGbl, tcg_rn_exports) +import RnNames (getMinimalImports, findImportUsage) +import Data.IORef (readIORef) +import Development.IDE.Core.PositionMapping (toCurrentRange, PositionMapping) +import PrelNames (pRELUDE) +import Ide.PluginUtils (mkLspCommand) +import Control.Monad (join) +import Data.List (intercalate) +import Data.Traversable (forM) +import Avail (availNamesWithSelectors, availNames, availName, AvailInfo(Avail)) +import Control.Arrow (Arrow(second)) +import Data.Aeson.Types +import qualified Data.HashMap.Strict as HashMap + +-- | plugin declaration +descriptor :: PluginId -> PluginDescriptor IdeState +descriptor plId = (defaultPluginDescriptor plId) + { pluginCodeLensProvider = Just lensProvider + , pluginCommands = [refineImportCommand] + , pluginRules = refineImportsRule + } + +refineImportCommandId :: CommandId +refineImportCommandId = "RefineImportLensCommand" + +newtype RefineImportCommandParams = RefineImportCommandParams WorkspaceEdit + deriving Generic + deriving anyclass (FromJSON, ToJSON) + +-- | The command descriptor +refineImportCommand :: PluginCommand IdeState +refineImportCommand = + PluginCommand + { commandId = refineImportCommandId + , commandDesc = "Directly use the imports as oppose to using aggregation module" + , commandFunc = runRefineImportCommand + } + +-- | The actual command handler +runRefineImportCommand :: CommandFunction IdeState RefineImportCommandParams +runRefineImportCommand _lspFuncs _state (RefineImportCommandParams edit) = do + -- This command simply triggers a workspace edit! + return (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams edit)) + +lensProvider :: CodeLensProvider IdeState +lensProvider + _lspFuncs + state -- ghcide state + pId -- plugin Id + CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} + -- VSCode uses URIs instead of file paths + -- haskell-lsp provides conversion functions + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = + do + mbRefinedImports <- runAction "RefineImports" state $ useWithStale RefineImports nfp + case mbRefinedImports of + -- Implement the provider logic: + -- for every import, if it's lacking a explicit list, generate a code lens + Just (RefineImportsResult result, posMapping) -> do + commands <- + sequence + [ generateLens pId _uri edit + | (imp, Just refinedImports) <- result + , Just edit <- [mkExplicitEdit posMapping imp refinedImports] + ] + return $ Right (List $ catMaybes commands) + _ -> return $ Right (List []) + | otherwise = + return $ Right (List []) + +data RefineImports = RefineImports + deriving (Show, Generic, Eq, Ord) + +instance Hashable RefineImports +instance NFData RefineImports +instance Binary RefineImports +type instance RuleResult RefineImports = RefineImportsResult + +newtype RefineImportsResult = RefineImportsResult + {getMinimalImportsResult :: [(LImportDecl GhcRn, Maybe T.Text)]} + +instance Show RefineImportsResult where show _ = "" +instance NFData RefineImportsResult where rnf = rwhnf + +refineImportsRule :: Rules () +refineImportsRule = define $ \RefineImports nfp -> do + -- Get the typechecking artifacts from the module + tmr <- use TypeCheck nfp + -- We also need a GHC session with all the dependencies + hsc <- use GhcSessionDeps nfp + + -- 2 layer map ModuleName -> ModuleName -> [Avails] (exports) + -- TODO make this parallelized better by using `uses` + import2Map <- do + -- first layer is from current(editing) module to its imports + ImportMap currIm <- use_ GetImportMap nfp + forM currIm $ \path -> do + -- second layer is from the imports of first layer to their imports + ImportMap importIm <- use_ GetImportMap path + forM importIm $ \imp_path -> do + imp_tmr <- use_ TypeCheck imp_path + return $ tcg_exports $ tmrTypechecked imp_tmr + + -- Use the GHC api to extract the "minimal" imports + -- We shouldn't blindly refine imports + -- instead we should generate imports statements for modules/symbols actually got used + (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr + + let filterByImport :: LImportDecl GhcRn -> Map.Map ModuleName [AvailInfo] -> Map.Map ModuleName [AvailInfo] + filterByImport (L _ ImportDecl{ideclHiding = Just (_, L _ names)}) avails = + let importedNames = map (prettyPrint . ieName . unLoc) names + in flip Map.filter avails $ \a -> + any ((`elem` importedNames) . prettyPrint) $ concatMap availNamesWithSelectors a + filterByImport _ _ = mempty + let constructImport :: LImportDecl GhcRn -> (ModuleName, [AvailInfo]) -> LImportDecl GhcRn + constructImport + i@(L lim id@ImportDecl{ideclName = L _ mn, ideclHiding = Just (hiding, L _ names)}) + (newModuleName, avails) = L lim id + { ideclName = noLoc newModuleName + , ideclHiding = Just (hiding, noLoc newNames) + } + where newNames = filter (\n -> any (n `containsAvail`) avails) names + constructImport lim _ = lim + let res = + [ (i, Just + . T.intercalate "\n" + . map (T.pack . prettyPrint . constructImport i) + . Map.toList + $ filteredInnerImports) + -- for every minimal imports + | Just minImports <- [mbMinImports] + , i@(L _ ImportDecl{ideclName = L _ mn}) <- minImports + -- we check for the inner imports + , Just innerImports <- [Map.lookup mn import2Map] + -- and only get those symbols used + , filteredInnerImports <- [filterByImport i innerImports] + -- if no symbols from this modules then don't need to generate new import + , not $ null filteredInnerImports + ] + return ([], RefineImportsResult res <$ mbMinImports) + + where + -- Check if a name is exposed by AvailInfo (the available information of a module) + containsAvail :: LIE GhcRn -> AvailInfo -> Bool + containsAvail name avail = + any (\an -> prettyPrint an == (prettyPrint . ieName . unLoc $ name)) + $ availNamesWithSelectors avail + +-------------------------------------------------------------------------------- + +-- | Use the ghc api to extract a minimal, explicit set of imports for this module +extractMinimalImports :: + Maybe HscEnvEq -> + Maybe TcModuleResult -> + IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn]) +extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do + -- extract the original imports and the typechecking environment + let tcEnv = tmrTypechecked + (_, imports, _, _) = tmrRenamed + ParsedModule {pm_parsed_source = L loc _} = tmrParsed + span = fromMaybe (error "expected real") $ realSpan loc + + -- GHC is secretly full of mutable state + gblElts <- readIORef (tcg_used_gres tcEnv) + + -- call findImportUsage does exactly what we need + -- GHC is full of treats like this + let usage = findImportUsage imports gblElts + (_, minimalImports) <- initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage + + -- return both the original imports and the computed minimal ones + return (imports, minimalImports) +extractMinimalImports _ _ = return ([], Nothing) + +mkExplicitEdit :: PositionMapping -> LImportDecl pass -> T.Text -> Maybe TextEdit +mkExplicitEdit posMapping (L src imp) explicit + | RealSrcSpan l <- src, + L _ mn <- ideclName imp, + -- (almost) no one wants to see an refine import list for Prelude + mn /= moduleName pRELUDE, + Just rng <- toCurrentRange posMapping $ realSrcSpanToRange l = + Just $ TextEdit rng explicit + | otherwise = + Nothing + +-- | Given an import declaration, generate a code lens unless it has an +-- explicit import list or it's qualified +generateLens :: PluginId -> Uri -> TextEdit -> IO (Maybe CodeLens) +generateLens pId uri edits@TextEdit {_range, _newText} = do + -- The title of the command is just the minimal explicit import decl + let title = T.intercalate ", " (T.lines _newText) + -- the code lens has no extra data + _xdata = Nothing + -- an edit that replaces the whole declaration with the explicit one + edit = WorkspaceEdit (Just editsMap) Nothing + editsMap = HashMap.fromList [(uri, List [edits])] + -- the command argument is simply the edit + _arguments = Just [toJSON $ RefineImportCommandParams edit] + -- create the command + _command <- Just <$> mkLspCommand pId refineImportCommandId title _arguments + -- create and return the code lens + return $ Just CodeLens {..} diff --git a/stack-8.8.4.yaml b/stack-8.8.4.yaml index 1cea93da82..0bb09a54b2 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.4.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin From b226961621734f29eb10a1a42f85325c0d6ccc47 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Wed, 7 Apr 2021 16:03:35 +0100 Subject: [PATCH 2/9] Implement code action for refine imports --- exe/Plugins.hs | 13 +- haskell-language-server.cabal | 12 +- .../hls-refine-imports-plugin.cabal | 8 +- .../src/Ide/Plugin/RefineImports.hs | 141 +++++++++++++----- 4 files changed, 129 insertions(+), 45 deletions(-) diff --git a/exe/Plugins.hs b/exe/Plugins.hs index 500509cd6b..d3c809c34c 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -26,8 +26,11 @@ import Ide.Plugin.Eval as Eval #endif #if importLens --- import Ide.Plugin.ExplicitImports as ExplicitImports -import Ide.Plugin.RefineImports as RefineImports +import Ide.Plugin.ExplicitImports as ExplicitImports +#endif + +#if refineImports +import Ide.Plugin.RefineImports as RefineImports #endif #if retrie @@ -124,8 +127,10 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins Eval.descriptor "eval" : #endif #if importLens - -- , ExplicitImports.descriptor "importLens" - , RefineImports.descriptor "refineImports" + ExplicitImports.descriptor "importLens" : +#endif +#if refineImports + RefineImports.descriptor "refineImports" : #endif #if moduleName ModuleName.descriptor "moduleName" : diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 0c640d9425..f2dc538e4f 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -123,6 +123,11 @@ flag importLens default: True manual: True +flag refineImports + description: Enable refineImports plugin + default: True + manual: True + flag retrie description: Enable retrie plugin default: True @@ -203,9 +208,13 @@ common eval common importLens if flag(importLens) || flag(all-plugins) build-depends: hls-explicit-imports-plugin ^>= 1.0.0.0 - hls-refine-imports-plugin cpp-options: -DimportLens +common refineImports + if flag(refineImports) || flag(all-plugins) + build-depends: hls-refine-imports-plugin + cpp-options: -DrefineImports + common retrie if flag(retrie) || flag(all-plugins) build-depends: hls-retrie-plugin ^>= 1.0.0.0 @@ -280,6 +289,7 @@ executable haskell-language-server , haddockComments , eval , importLens + , refineImports , retrie , tactic , hlint diff --git a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal index 3e8508a682..42e1ea64ac 100644 --- a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal +++ b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal @@ -16,12 +16,14 @@ library , base >=4.12 && <5 , containers , deepseq + , lsp , haskell-lsp-types - , hls-plugin-api + , hls-plugin-api >= 1.0 && < 1.2 , ghc - , ghcide + , ghcide ^>= 1.1.0.0 , shake , text , unordered-containers - default-language: Haskell2010 \ No newline at end of file + default-language: Haskell2010 + default-extensions: DataKinds, TypeOperators \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs index 775829405d..412bcc8e8a 100644 --- a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -6,39 +6,46 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DuplicateRecordFields #-} + module Ide.Plugin.RefineImports (descriptor) where -import Development.IDE -import Ide.Types -import Language.Haskell.LSP.Types -import Development.Shake.Classes -import GHC.Generics (Generic) -import Development.IDE.GHC.Compat -import qualified Data.Map.Strict as Map -import qualified Data.Text as T -import Data.Maybe (fromMaybe, catMaybes) -import Control.Monad.IO.Class (liftIO) +import Avail (AvailInfo (Avail), availName, availNames, availNamesWithSelectors) +import Control.Arrow (Arrow (second)) import Control.DeepSeq (rwhnf) -import TcRnMonad (tcg_used_gres, initTcWithGbl, tcg_rn_exports) -import RnNames (getMinimalImports, findImportUsage) -import Data.IORef (readIORef) -import Development.IDE.Core.PositionMapping (toCurrentRange, PositionMapping) -import PrelNames (pRELUDE) -import Ide.PluginUtils (mkLspCommand) import Control.Monad (join) -import Data.List (intercalate) -import Data.Traversable (forM) -import Avail (availNamesWithSelectors, availNames, availName, AvailInfo(Avail)) -import Control.Arrow (Arrow(second)) +import Control.Monad.IO.Class (liftIO) import Data.Aeson.Types import qualified Data.HashMap.Strict as HashMap +import Data.IORef (readIORef) +import Data.List (intercalate) +import qualified Data.Map.Strict as Map +import Data.Maybe (catMaybes, fromMaybe) +import qualified Data.Text as T +import Data.Traversable (forM) +import Development.IDE +import Development.IDE.Core.PositionMapping +import Development.IDE.GHC.Compat +import Development.Shake.Classes +import GHC.Generics (Generic) +import Ide.PluginUtils (mkLspCommand) +import Ide.Types +import Language.LSP.Server +import Language.LSP.Types +import PrelNames (pRELUDE) +import RnNames (findImportUsage, getMinimalImports) +import TcRnMonad (initTcWithGbl, tcg_rn_exports, tcg_used_gres) -- | plugin declaration descriptor :: PluginId -> PluginDescriptor IdeState descriptor plId = (defaultPluginDescriptor plId) - { pluginCodeLensProvider = Just lensProvider - , pluginCommands = [refineImportCommand] + { pluginCommands = [refineImportCommand] , pluginRules = refineImportsRule + , pluginHandlers = mconcat + [ -- This plugin provides code lenses + mkPluginHandler STextDocumentCodeLens lensProvider + -- This plugin provides code actions + , mkPluginHandler STextDocumentCodeAction codeActionProvider + ] } refineImportCommandId :: CommandId @@ -59,21 +66,22 @@ refineImportCommand = -- | The actual command handler runRefineImportCommand :: CommandFunction IdeState RefineImportCommandParams -runRefineImportCommand _lspFuncs _state (RefineImportCommandParams edit) = do +runRefineImportCommand _state (RefineImportCommandParams edit) = do -- This command simply triggers a workspace edit! - return (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams edit)) + _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + return (Right Null) -lensProvider :: CodeLensProvider IdeState +lensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens lensProvider - _lspFuncs state -- ghcide state pId -- plugin Id CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} -- VSCode uses URIs instead of file paths -- haskell-lsp provides conversion functions - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = liftIO $ do - mbRefinedImports <- runAction "RefineImports" state $ useWithStale RefineImports nfp + mbRefinedImports <- + runIde state $ useWithStale RefineImports nfp case mbRefinedImports of -- Implement the provider logic: -- for every import, if it's lacking a explicit list, generate a code lens @@ -89,6 +97,46 @@ lensProvider | otherwise = return $ Right (List []) +-- | If there are any implicit imports, provide one code action to turn them all +-- into explicit imports. +codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction +codeActionProvider ideState _pId (CodeActionParams _ _ docId range _context) + | TextDocumentIdentifier {_uri} <- docId, + Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = liftIO $ + do + pm <- runIde ideState $ use GetParsedModule nfp + let insideImport = case pm of + Just ParsedModule {pm_parsed_source} + | locImports <- hsmodImports (unLoc pm_parsed_source), + rangesImports <- map getLoc locImports -> + any (within range) rangesImports + _ -> False + if not insideImport + then return (Right (List [])) + else do + mbRefinedImports <- runIde ideState $ use RefineImports nfp + let edits = + [ e + | Just (RefineImportsResult result) <- [mbRefinedImports] + , (imp, Just refinedImports) <- result + , Just e <- [mkExplicitEdit zeroMapping imp refinedImports] + ] + caExplicitImports = InR CodeAction {..} + _title = "Refine all imports" + _kind = Just CodeActionQuickFix + _command = Nothing + _edit = Just WorkspaceEdit {_changes, _documentChanges} + _changes = Just $ HashMap.singleton _uri $ List edits + _documentChanges = Nothing + _diagnostics = Nothing + _isPreferred = Nothing + _disabled = Nothing + return $ Right $ List [caExplicitImports | not (null edits)] + | otherwise = + return $ Right $ List [] + +-------------------------------------------------------------------------------- + data RefineImports = RefineImports deriving (Show, Generic, Eq, Ord) @@ -111,7 +159,6 @@ refineImportsRule = define $ \RefineImports nfp -> do hsc <- use GhcSessionDeps nfp -- 2 layer map ModuleName -> ModuleName -> [Avails] (exports) - -- TODO make this parallelized better by using `uses` import2Map <- do -- first layer is from current(editing) module to its imports ImportMap currIm <- use_ GetImportMap nfp @@ -124,18 +171,27 @@ refineImportsRule = define $ \RefineImports nfp -> do -- Use the GHC api to extract the "minimal" imports -- We shouldn't blindly refine imports - -- instead we should generate imports statements for modules/symbols actually got used + -- instead we should generate imports statements + -- for modules/symbols actually got used (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr - let filterByImport :: LImportDecl GhcRn -> Map.Map ModuleName [AvailInfo] -> Map.Map ModuleName [AvailInfo] + let filterByImport + :: LImportDecl GhcRn + -> Map.Map ModuleName [AvailInfo] + -> Map.Map ModuleName [AvailInfo] filterByImport (L _ ImportDecl{ideclHiding = Just (_, L _ names)}) avails = let importedNames = map (prettyPrint . ieName . unLoc) names in flip Map.filter avails $ \a -> - any ((`elem` importedNames) . prettyPrint) $ concatMap availNamesWithSelectors a + any ((`elem` importedNames) . prettyPrint) + $ concatMap availNamesWithSelectors a filterByImport _ _ = mempty - let constructImport :: LImportDecl GhcRn -> (ModuleName, [AvailInfo]) -> LImportDecl GhcRn + let constructImport + :: LImportDecl GhcRn + -> (ModuleName, [AvailInfo]) + -> LImportDecl GhcRn constructImport - i@(L lim id@ImportDecl{ideclName = L _ mn, ideclHiding = Just (hiding, L _ names)}) + i@(L lim id@ImportDecl + {ideclName = L _ mn, ideclHiding = Just (hiding, L _ names)}) (newModuleName, avails) = L lim id { ideclName = noLoc newModuleName , ideclHiding = Just (hiding, noLoc newNames) @@ -187,7 +243,8 @@ extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do -- call findImportUsage does exactly what we need -- GHC is full of treats like this let usage = findImportUsage imports gblElts - (_, minimalImports) <- initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage + (_, minimalImports) <- + initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage -- return both the original imports and the computed minimal ones return (imports, minimalImports) @@ -217,7 +274,17 @@ generateLens pId uri edits@TextEdit {_range, _newText} = do editsMap = HashMap.fromList [(uri, List [edits])] -- the command argument is simply the edit _arguments = Just [toJSON $ RefineImportCommandParams edit] - -- create the command - _command <- Just <$> mkLspCommand pId refineImportCommandId title _arguments + -- create the command + _command = Just $ mkLspCommand pId refineImportCommandId title _arguments -- create and return the code lens return $ Just CodeLens {..} + +-------------------------------------------------------------------------------- + +-- | A helper to run ide actions +runIde :: IdeState -> Action a -> IO a +runIde = runAction "RefineImports" + +within :: Range -> SrcSpan -> Bool +within (Range start end) span = + isInsideSrcSpan start span || isInsideSrcSpan end span \ No newline at end of file From 77322ab245b1af42d7d5188e5a5e30deb440edb5 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Wed, 7 Apr 2021 16:21:39 +0100 Subject: [PATCH 3/9] fix stack.yaml --- stack.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/stack.yaml b/stack.yaml index 3e681a6bcc..27dc1b665b 100644 --- a/stack.yaml +++ b/stack.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin From d90de727dd70679ef07a47b653b2cfbf08cad909 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Wed, 7 Apr 2021 16:27:44 +0100 Subject: [PATCH 4/9] fix stack-*.yaml --- stack-8.10.2.yaml | 1 + stack-8.10.3.yaml | 1 + stack-8.10.4.yaml | 1 + stack-8.6.4.yaml | 1 + stack-8.6.5.yaml | 1 + stack-8.8.2.yaml | 1 + stack-8.8.3.yaml | 1 + 7 files changed, 7 insertions(+) diff --git a/stack-8.10.2.yaml b/stack-8.10.2.yaml index 1d11d7019c..a53ba2991f 100644 --- a/stack-8.10.2.yaml +++ b/stack-8.10.2.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.10.3.yaml b/stack-8.10.3.yaml index dbd1447991..30694fcc9b 100644 --- a/stack-8.10.3.yaml +++ b/stack-8.10.3.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.10.4.yaml b/stack-8.10.4.yaml index d8cb1cc319..330600023a 100644 --- a/stack-8.10.4.yaml +++ b/stack-8.10.4.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.6.4.yaml b/stack-8.6.4.yaml index 6ee16c62b9..583d190278 100644 --- a/stack-8.6.4.yaml +++ b/stack-8.6.4.yaml @@ -12,6 +12,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.6.5.yaml b/stack-8.6.5.yaml index 8ba6ff4853..e8df799423 100644 --- a/stack-8.6.5.yaml +++ b/stack-8.6.5.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.8.2.yaml b/stack-8.8.2.yaml index 665746a4bd..a9f6272e5b 100644 --- a/stack-8.8.2.yaml +++ b/stack-8.8.2.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin diff --git a/stack-8.8.3.yaml b/stack-8.8.3.yaml index 9decdcf0ea..2fc15ff328 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.yaml @@ -11,6 +11,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin From 38ed76854fc319ad8a18c5e87531512837f84648 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Wed, 7 Apr 2021 16:42:24 +0100 Subject: [PATCH 5/9] fix missing records fields after merge master --- .../src/Ide/Plugin/RefineImports.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs index 412bcc8e8a..a0b78e8f4d 100644 --- a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -125,12 +125,15 @@ codeActionProvider ideState _pId (CodeActionParams _ _ docId range _context) _title = "Refine all imports" _kind = Just CodeActionQuickFix _command = Nothing - _edit = Just WorkspaceEdit {_changes, _documentChanges} + _edit = Just WorkspaceEdit + {_changes, _documentChanges, _changeAnnotations} _changes = Just $ HashMap.singleton _uri $ List edits _documentChanges = Nothing _diagnostics = Nothing _isPreferred = Nothing _disabled = Nothing + _xdata = Nothing + _changeAnnotations = Nothing return $ Right $ List [caExplicitImports | not (null edits)] | otherwise = return $ Right $ List [] @@ -270,7 +273,7 @@ generateLens pId uri edits@TextEdit {_range, _newText} = do -- the code lens has no extra data _xdata = Nothing -- an edit that replaces the whole declaration with the explicit one - edit = WorkspaceEdit (Just editsMap) Nothing + edit = WorkspaceEdit (Just editsMap) Nothing Nothing editsMap = HashMap.fromList [(uri, List [edits])] -- the command argument is simply the edit _arguments = Just [toJSON $ RefineImportCommandParams edit] From 612273decf2fd24ca7472b484a875d458a5280fd Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Thu, 8 Apr 2021 15:21:16 +0100 Subject: [PATCH 6/9] fix nix --- nix/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/default.nix b/nix/default.nix index 713cf9607f..0677c2b3a7 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -35,6 +35,7 @@ let hls-haddock-comments-plugin = gitignoreSource ../plugins/hls-haddock-comments-plugin; hls-eval-plugin = gitignoreSource ../plugins/hls-eval-plugin; hls-explicit-imports-plugin = gitignoreSource ../plugins/hls-explicit-imports-plugin; + hls-refine-imports-plugin = gitignoreSource ../plugins/hls-refine-imports-plugin; hls-hlint-plugin = gitignoreSource ../plugins/hls-hlint-plugin; hls-retrie-plugin = gitignoreSource ../plugins/hls-retrie-plugin; hls-splice-plugin = gitignoreSource ../plugins/hls-splice-plugin; From 2d3309b4091db61e7ce70e95fa193acd05740844 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Thu, 8 Apr 2021 16:58:31 +0100 Subject: [PATCH 7/9] reduce duplicated functions --- .../src/Ide/Plugin/ExplicitImports.hs | 15 +- .../hls-refine-imports-plugin.cabal | 5 +- .../src/Ide/Plugin/RefineImports.hs | 140 ++++++++---------- 3 files changed, 72 insertions(+), 88 deletions(-) diff --git a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs index 739605c5da..a80b153f5a 100644 --- a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs +++ b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs @@ -11,7 +11,11 @@ #include "ghc-api-version.h" -module Ide.Plugin.ExplicitImports (descriptor) where +module Ide.Plugin.ExplicitImports + ( descriptor + , extractMinimalImports + , within + ) where import Control.DeepSeq import Control.Monad.IO.Class @@ -210,7 +214,8 @@ extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do -- call findImportUsage does exactly what we need -- GHC is full of treats like this let usage = findImportUsage imports gblElts - (_, minimalImports) <- initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage + (_, minimalImports) <- + initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage -- return both the original imports and the computed minimal ones return (imports, minimalImports) @@ -249,11 +254,11 @@ generateLens pId uri importEdit@TextEdit {_range, _newText} = do -- create and return the code lens return $ Just CodeLens {..} +-------------------------------------------------------------------------------- + -- | A helper to run ide actions runIde :: IdeState -> Action a -> IO a -runIde state = runAction "importLens" state - --------------------------------------------------------------------------------- +runIde = runAction "importLens" within :: Range -> SrcSpan -> Bool within (Range start end) span = diff --git a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal index 42e1ea64ac..2c1acb16fd 100644 --- a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal +++ b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal @@ -17,13 +17,14 @@ library , containers , deepseq , lsp - , haskell-lsp-types + , lsp-types , hls-plugin-api >= 1.0 && < 1.2 , ghc , ghcide ^>= 1.1.0.0 , shake , text , unordered-containers + , hls-explicit-imports-plugin ^>= 1.0.0.0 default-language: Haskell2010 - default-extensions: DataKinds, TypeOperators \ No newline at end of file + default-extensions: DataKinds, TypeOperators diff --git a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs index a0b78e8f4d..c809429468 100644 --- a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -1,39 +1,46 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Plugin.RefineImports (descriptor) where -import Avail (AvailInfo (Avail), availName, availNames, availNamesWithSelectors) -import Control.Arrow (Arrow (second)) -import Control.DeepSeq (rwhnf) -import Control.Monad (join) -import Control.Monad.IO.Class (liftIO) -import Data.Aeson.Types -import qualified Data.HashMap.Strict as HashMap -import Data.IORef (readIORef) -import Data.List (intercalate) -import qualified Data.Map.Strict as Map -import Data.Maybe (catMaybes, fromMaybe) -import qualified Data.Text as T -import Data.Traversable (forM) -import Development.IDE -import Development.IDE.Core.PositionMapping -import Development.IDE.GHC.Compat -import Development.Shake.Classes -import GHC.Generics (Generic) -import Ide.PluginUtils (mkLspCommand) -import Ide.Types -import Language.LSP.Server -import Language.LSP.Types -import PrelNames (pRELUDE) -import RnNames (findImportUsage, getMinimalImports) -import TcRnMonad (initTcWithGbl, tcg_rn_exports, tcg_used_gres) +import Avail (AvailInfo (Avail), + availName, availNames, + availNamesWithSelectors) +import Control.Arrow (Arrow (second)) +import Control.DeepSeq (rwhnf) +import Control.Monad (join) +import Control.Monad.IO.Class (liftIO) +import Data.Aeson.Types +import qualified Data.HashMap.Strict as HashMap +import Data.IORef (readIORef) +import Data.List (intercalate) +import qualified Data.Map.Strict as Map +import Data.Maybe (catMaybes, fromMaybe) +import qualified Data.Text as T +import Data.Traversable (forM) +import Development.IDE +import Development.IDE.Core.PositionMapping +import Development.IDE.GHC.Compat +import Development.Shake.Classes +import GHC.Generics (Generic) +import Ide.Plugin.ExplicitImports (extractMinimalImports, + within) +import Ide.PluginUtils (mkLspCommand) +import Ide.Types +import Language.LSP.Server +import Language.LSP.Types +import PrelNames (pRELUDE) +import RnNames (findImportUsage, + getMinimalImports) +import TcRnMonad (initTcWithGbl, + tcg_rn_exports, + tcg_used_gres) -- | plugin declaration descriptor :: PluginId -> PluginDescriptor IdeState @@ -48,7 +55,7 @@ descriptor plId = (defaultPluginDescriptor plId) ] } -refineImportCommandId :: CommandId +refineImportCommandId :: CommandId refineImportCommandId = "RefineImportLensCommand" newtype RefineImportCommandParams = RefineImportCommandParams WorkspaceEdit @@ -57,7 +64,7 @@ newtype RefineImportCommandParams = RefineImportCommandParams WorkspaceEdit -- | The command descriptor refineImportCommand :: PluginCommand IdeState -refineImportCommand = +refineImportCommand = PluginCommand { commandId = refineImportCommandId , commandDesc = "Directly use the imports as oppose to using aggregation module" @@ -74,13 +81,13 @@ runRefineImportCommand _state (RefineImportCommandParams edit) = do lensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens lensProvider state -- ghcide state - pId -- plugin Id + pId CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} -- VSCode uses URIs instead of file paths -- haskell-lsp provides conversion functions | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = liftIO $ do - mbRefinedImports <- + mbRefinedImports <- runIde state $ useWithStale RefineImports nfp case mbRefinedImports of -- Implement the provider logic: @@ -125,7 +132,7 @@ codeActionProvider ideState _pId (CodeActionParams _ _ docId range _context) _title = "Refine all imports" _kind = Just CodeActionQuickFix _command = Nothing - _edit = Just WorkspaceEdit + _edit = Just WorkspaceEdit {_changes, _documentChanges, _changeAnnotations} _changes = Just $ HashMap.singleton _uri $ List edits _documentChanges = Nothing @@ -165,7 +172,7 @@ refineImportsRule = define $ \RefineImports nfp -> do import2Map <- do -- first layer is from current(editing) module to its imports ImportMap currIm <- use_ GetImportMap nfp - forM currIm $ \path -> do + forM currIm $ \path -> do -- second layer is from the imports of first layer to their imports ImportMap importIm <- use_ GetImportMap path forM importIm $ \imp_path -> do @@ -174,25 +181,25 @@ refineImportsRule = define $ \RefineImports nfp -> do -- Use the GHC api to extract the "minimal" imports -- We shouldn't blindly refine imports - -- instead we should generate imports statements + -- instead we should generate imports statements -- for modules/symbols actually got used (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr - let filterByImport - :: LImportDecl GhcRn - -> Map.Map ModuleName [AvailInfo] + let filterByImport + :: LImportDecl GhcRn + -> Map.Map ModuleName [AvailInfo] -> Map.Map ModuleName [AvailInfo] filterByImport (L _ ImportDecl{ideclHiding = Just (_, L _ names)}) avails = let importedNames = map (prettyPrint . ieName . unLoc) names in flip Map.filter avails $ \a -> - any ((`elem` importedNames) . prettyPrint) + any ((`elem` importedNames) . prettyPrint) $ concatMap availNamesWithSelectors a filterByImport _ _ = mempty - let constructImport - :: LImportDecl GhcRn - -> (ModuleName, [AvailInfo]) + let constructImport + :: LImportDecl GhcRn + -> (ModuleName, [AvailInfo]) -> LImportDecl GhcRn - constructImport + constructImport i@(L lim id@ImportDecl {ideclName = L _ mn, ideclHiding = Just (hiding, L _ names)}) (newModuleName, avails) = L lim id @@ -203,9 +210,9 @@ refineImportsRule = define $ \RefineImports nfp -> do constructImport lim _ = lim let res = [ (i, Just - . T.intercalate "\n" - . map (T.pack . prettyPrint . constructImport i) - . Map.toList + . T.intercalate "\n" + . map (T.pack . prettyPrint . constructImport i) + . Map.toList $ filteredInnerImports) -- for every minimal imports | Just minImports <- [mbMinImports] @@ -222,37 +229,12 @@ refineImportsRule = define $ \RefineImports nfp -> do where -- Check if a name is exposed by AvailInfo (the available information of a module) containsAvail :: LIE GhcRn -> AvailInfo -> Bool - containsAvail name avail = - any (\an -> prettyPrint an == (prettyPrint . ieName . unLoc $ name)) + containsAvail name avail = + any (\an -> prettyPrint an == (prettyPrint . ieName . unLoc $ name)) $ availNamesWithSelectors avail -------------------------------------------------------------------------------- --- | Use the ghc api to extract a minimal, explicit set of imports for this module -extractMinimalImports :: - Maybe HscEnvEq -> - Maybe TcModuleResult -> - IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn]) -extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do - -- extract the original imports and the typechecking environment - let tcEnv = tmrTypechecked - (_, imports, _, _) = tmrRenamed - ParsedModule {pm_parsed_source = L loc _} = tmrParsed - span = fromMaybe (error "expected real") $ realSpan loc - - -- GHC is secretly full of mutable state - gblElts <- readIORef (tcg_used_gres tcEnv) - - -- call findImportUsage does exactly what we need - -- GHC is full of treats like this - let usage = findImportUsage imports gblElts - (_, minimalImports) <- - initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage - - -- return both the original imports and the computed minimal ones - return (imports, minimalImports) -extractMinimalImports _ _ = return ([], Nothing) - mkExplicitEdit :: PositionMapping -> LImportDecl pass -> T.Text -> Maybe TextEdit mkExplicitEdit posMapping (L src imp) explicit | RealSrcSpan l <- src, @@ -287,7 +269,3 @@ generateLens pId uri edits@TextEdit {_range, _newText} = do -- | A helper to run ide actions runIde :: IdeState -> Action a -> IO a runIde = runAction "RefineImports" - -within :: Range -> SrcSpan -> Bool -within (Range start end) span = - isInsideSrcSpan start span || isInsideSrcSpan end span \ No newline at end of file From 2ceec2d08bb2e09d3a62008d8402ce34cb2f176d Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Tue, 27 Apr 2021 13:43:41 +0100 Subject: [PATCH 8/9] [Plugin][RefineImports] add tests and fix overriding issue --- .../hls-refine-imports-plugin.cabal | 45 +++++++--- .../src/Ide/Plugin/RefineImports.hs | 27 ++++-- .../hls-refine-imports-plugin/test/Main.hs | 89 +++++++++++++++++++ .../test/testdata/A.hs | 7 ++ .../test/testdata/B.hs | 7 ++ .../test/testdata/C.hs | 4 + .../test/testdata/D.hs | 7 ++ .../test/testdata/E.hs | 7 ++ .../test/testdata/UsualCase.expected.hs | 10 +++ .../test/testdata/UsualCase.hs | 10 +++ .../test/testdata/WithOverride.expected.hs | 11 +++ .../test/testdata/WithOverride.hs | 10 +++ .../test/testdata/hie.yaml | 10 +++ 13 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 plugins/hls-refine-imports-plugin/test/Main.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/A.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/B.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/C.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/D.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/E.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/UsualCase.expected.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/UsualCase.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/WithOverride.expected.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/WithOverride.hs create mode 100644 plugins/hls-refine-imports-plugin/test/testdata/hie.yaml diff --git a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal index 2c1acb16fd..c79fd06053 100644 --- a/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal +++ b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal @@ -1,6 +1,6 @@ cabal-version: 2.2 name: hls-refine-imports-plugin -version: 0.1.0.0 +version: 1.0.0.0 synopsis: Refine imports plugin for Haskell Language Server license: Apache-2.0 license-file: LICENSE @@ -8,23 +8,40 @@ author: rayshih maintainer: mnf.shih@gmail.com category: Development build-type: Simple +extra-source-files: + LICENSE library exposed-modules: Ide.Plugin.RefineImports hs-source-dirs: src - build-depends: aeson - , base >=4.12 && <5 - , containers - , deepseq - , lsp - , lsp-types - , hls-plugin-api >= 1.0 && < 1.2 - , ghc - , ghcide ^>= 1.1.0.0 - , shake - , text - , unordered-containers - , hls-explicit-imports-plugin ^>= 1.0.0.0 + build-depends: + , aeson + , base >=4.12 && <5 + , containers + , deepseq + , ghc + , ghcide ^>=1.2.0.2 + , hls-plugin-api ^>=1.1.0.0 + , lsp + , lsp-types + , hls-graph + , text + , unordered-containers + , hls-explicit-imports-plugin ^>= 1.0.0.1 default-language: Haskell2010 default-extensions: DataKinds, TypeOperators + +test-suite tests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: test + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + , base + , bytestring + , filepath + , hls-refine-imports-plugin + , hls-test-utils + , text diff --git a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs index c809429468..d5528e7e84 100644 --- a/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -26,8 +26,18 @@ import qualified Data.Text as T import Data.Traversable (forM) import Development.IDE import Development.IDE.Core.PositionMapping -import Development.IDE.GHC.Compat -import Development.Shake.Classes +import Development.IDE.GHC.Compat (AvailInfo, + GenLocated (L), GhcRn, + HsModule (hsmodImports), + ImportDecl (ImportDecl, ideclHiding, ideclName), + LIE, LImportDecl, + Module (moduleName), + ModuleName, + ParsedModule (ParsedModule, pm_parsed_source), + SrcSpan (RealSrcSpan), + getLoc, ieName, noLoc, + tcg_exports, unLoc) +import Development.IDE.Graph.Classes import GHC.Generics (Generic) import Ide.Plugin.ExplicitImports (extractMinimalImports, within) @@ -91,7 +101,7 @@ lensProvider runIde state $ useWithStale RefineImports nfp case mbRefinedImports of -- Implement the provider logic: - -- for every import, if it's lacking a explicit list, generate a code lens + -- for every refined import, generate a code lens Just (RefineImportsResult result, posMapping) -> do commands <- sequence @@ -104,8 +114,7 @@ lensProvider | otherwise = return $ Right (List []) --- | If there are any implicit imports, provide one code action to turn them all --- into explicit imports. +-- | Provide one code action to refine all imports codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction codeActionProvider ideState _pId (CodeActionParams _ _ docId range _context) | TextDocumentIdentifier {_uri} <- docId, @@ -130,7 +139,7 @@ codeActionProvider ideState _pId (CodeActionParams _ _ docId range _context) ] caExplicitImports = InR CodeAction {..} _title = "Refine all imports" - _kind = Just CodeActionQuickFix + _kind = Just $ CodeActionUnknown "quickfix.import.refine" _command = Nothing _edit = Just WorkspaceEdit {_changes, _documentChanges, _changeAnnotations} @@ -190,9 +199,9 @@ refineImportsRule = define $ \RefineImports nfp -> do -> Map.Map ModuleName [AvailInfo] -> Map.Map ModuleName [AvailInfo] filterByImport (L _ ImportDecl{ideclHiding = Just (_, L _ names)}) avails = - let importedNames = map (prettyPrint . ieName . unLoc) names + let importedNames = map (ieName . unLoc) names in flip Map.filter avails $ \a -> - any ((`elem` importedNames) . prettyPrint) + any (`elem` importedNames) $ concatMap availNamesWithSelectors a filterByImport _ _ = mempty let constructImport @@ -251,7 +260,7 @@ mkExplicitEdit posMapping (L src imp) explicit generateLens :: PluginId -> Uri -> TextEdit -> IO (Maybe CodeLens) generateLens pId uri edits@TextEdit {_range, _newText} = do -- The title of the command is just the minimal explicit import decl - let title = T.intercalate ", " (T.lines _newText) + let title = "Refine imports to " <> T.intercalate ", " (T.lines _newText) -- the code lens has no extra data _xdata = Nothing -- an edit that replaces the whole declaration with the explicit one diff --git a/plugins/hls-refine-imports-plugin/test/Main.hs b/plugins/hls-refine-imports-plugin/test/Main.hs new file mode 100644 index 0000000000..a63f63354a --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/Main.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE DisambiguateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ViewPatterns #-} + +module Main (main) where + +import qualified Data.ByteString.Lazy as LBS +import Data.Foldable (find, forM_) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import qualified Ide.Plugin.RefineImports as RefineImports +import System.FilePath ((<.>), ()) +import Test.Hls + +main :: IO () +main = defaultTestRunner $ + testGroup + "Refine Imports" + [ codeActionGoldenTest "WithOverride" 3 1 + , codeLensGoldenTest "UsualCase" 1 + ] + +plugin :: PluginDescriptor IdeState +plugin = RefineImports.descriptor "refineImports" + +-- code action tests + +codeActionGoldenTest :: FilePath -> Int -> Int -> TestTree +codeActionGoldenTest fp l c = goldenGitDiff (fp <> " (golden)") goldenFilePath $ + runSessionWithServer plugin testDataDir $ do + doc <- openDoc hsFilePath "haskell" + actions <- getCodeActions doc (pointRange l c) + case find ((== Just "Refine all imports") . caTitle) actions of + Just (InR x) -> do + executeCodeAction x + LBS.fromStrict . encodeUtf8 <$> documentContents doc + _ -> liftIO $ assertFailure "Unable to find CodeAction" + where + hsFilePath = fp <.> "hs" + goldenFilePath = testDataDir fp <.> "expected" <.> "hs" + +caTitle :: (Command |? CodeAction) -> Maybe Text +caTitle (InR CodeAction {_title}) = Just _title +caTitle _ = Nothing + + +-- code lens tests + +codeLensGoldenTest :: FilePath -> Int -> TestTree +codeLensGoldenTest fp codeLensIdx = goldenGitDiff (fp <> " (golden)") goldenFilePath $ + runSessionWithServer plugin testDataDir $ do + doc <- openDoc hsFilePath "haskell" + codeLens <- (!! codeLensIdx) <$> getCodeLensesBy isRefineImports doc + mapM_ executeCmd + [c | CodeLens{_command = Just c} <- [codeLens]] + LBS.fromStrict . encodeUtf8 <$> documentContents doc + where + hsFilePath = fp <.> "hs" + goldenFilePath = testDataDir fp <.> "expected" <.> "hs" + +getCodeLensesBy :: (CodeLens -> Bool) -> TextDocumentIdentifier -> Session [CodeLens] +getCodeLensesBy f doc = filter f <$> getCodeLenses doc + +isRefineImports :: CodeLens -> Bool +isRefineImports (CodeLens _ (Just (Command _ cmd _)) _) + | ":refineImports:" `T.isInfixOf` cmd = True +isRefineImports _ = False + +-- Execute command and wait for result +executeCmd :: Command -> Session () +executeCmd cmd = do + executeCommand cmd + _resp <- skipManyTill anyMessage (message SWorkspaceApplyEdit) + -- liftIO $ print _resp + return () + +-- helpers + +testDataDir :: String +testDataDir = "test" "testdata" + +pointRange :: Int -> Int -> Range +pointRange + (subtract 1 -> line) + (subtract 1 -> col) = + Range (Position line col) (Position line $ col + 1) diff --git a/plugins/hls-refine-imports-plugin/test/testdata/A.hs b/plugins/hls-refine-imports-plugin/test/testdata/A.hs new file mode 100644 index 0000000000..da94829c76 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/A.hs @@ -0,0 +1,7 @@ +module A + ( module B + , module C + ) where + +import B +import C \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/test/testdata/B.hs b/plugins/hls-refine-imports-plugin/test/testdata/B.hs new file mode 100644 index 0000000000..a813ff528a --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/B.hs @@ -0,0 +1,7 @@ +module B where + +b1 :: String +b1 = "b1" + +b2 :: String +b2 = "b2" \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/test/testdata/C.hs b/plugins/hls-refine-imports-plugin/test/testdata/C.hs new file mode 100644 index 0000000000..28434310d2 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/C.hs @@ -0,0 +1,4 @@ +module C where + +c1 :: String +c1 = "c1" \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/test/testdata/D.hs b/plugins/hls-refine-imports-plugin/test/testdata/D.hs new file mode 100644 index 0000000000..afb002ca84 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/D.hs @@ -0,0 +1,7 @@ +module D (module E, module D) where + +import E hiding (e1) +import qualified E + +e1 :: String +e1 = E.e1 <> " but overrided" \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/test/testdata/E.hs b/plugins/hls-refine-imports-plugin/test/testdata/E.hs new file mode 100644 index 0000000000..7f61954f30 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/E.hs @@ -0,0 +1,7 @@ +module E where + +e1 :: String +e1 = "e1" + +e2 :: String +e2 = "e2" \ No newline at end of file diff --git a/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.expected.hs b/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.expected.hs new file mode 100644 index 0000000000..6403caef33 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.expected.hs @@ -0,0 +1,10 @@ +module Main where + +import A +import E ( e2 ) +import Data.List (intercalate) + +main :: IO () +main = putStrLn + $ "hello " + <> intercalate ", " [b1, c1, e2] diff --git a/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.hs b/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.hs new file mode 100644 index 0000000000..cb8193d35d --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/UsualCase.hs @@ -0,0 +1,10 @@ +module Main where + +import A +import D +import Data.List (intercalate) + +main :: IO () +main = putStrLn + $ "hello " + <> intercalate ", " [b1, c1, e2] diff --git a/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.expected.hs b/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.expected.hs new file mode 100644 index 0000000000..c743d4d110 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.expected.hs @@ -0,0 +1,11 @@ +module Main where + +import B ( b1 ) +import C ( c1 ) +import D +import Data.List (intercalate) + +main :: IO () +main = putStrLn + $ "hello " + <> intercalate ", " [b1, c1, e1] diff --git a/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.hs b/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.hs new file mode 100644 index 0000000000..e25fa41bea --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/WithOverride.hs @@ -0,0 +1,10 @@ +module Main where + +import A +import D +import Data.List (intercalate) + +main :: IO () +main = putStrLn + $ "hello " + <> intercalate ", " [b1, c1, e1] diff --git a/plugins/hls-refine-imports-plugin/test/testdata/hie.yaml b/plugins/hls-refine-imports-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..0d1383c68f --- /dev/null +++ b/plugins/hls-refine-imports-plugin/test/testdata/hie.yaml @@ -0,0 +1,10 @@ +cradle: + direct: + arguments: + - UsualCase.hs + - WithOverride.hs + - A.hs + - B.hs + - C.hs + - D.hs + - E.hs \ No newline at end of file From 46c089603b8aa2e2994c2d4d9dccc37e2b835308 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Thu, 29 Apr 2021 01:07:14 +0100 Subject: [PATCH 9/9] add hls-refine-imports-plugin to CI workflow --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4be3b656de..3b394f74dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -173,3 +173,7 @@ jobs: - if: ${{ needs.pre_job.outputs.should_skip != 'true' && matrix.test }} name: Test hls-tactics-plugin test suite run: cabal test hls-tactics-plugin --test-options="-j1 --rerun-update" || cabal test hls-tactics-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-tactics-plugin --test-options="-j1 --rerun" + + - if: ${{ needs.pre_job.outputs.should_skip != 'true' && matrix.test }} + name: Test hls-refine-imports-plugin test suite + run: cabal test hls-refine-imports-plugin --test-options="-j1 --rerun-update" || cabal test hls-refine-imports-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-refine-imports-plugin --test-options="-j1 --rerun"