From 54737e9e2d455cccbef8df7eeba8d9fe7cece1d7 Mon Sep 17 00:00:00 2001 From: Ray Shih Date: Fri, 30 Apr 2021 03:32:19 +0100 Subject: [PATCH] Implement refine imports (#1686) * Implement refine imports * Implement code action for refine imports * fix stack.yaml * fix stack-*.yaml * fix missing records fields after merge master * fix nix * reduce duplicated functions * [Plugin][RefineImports] add tests and fix overriding issue * add hls-refine-imports-plugin to CI workflow Co-authored-by: Junyoung/Clare Jang --- .github/workflows/test.yml | 4 + cabal.project | 1 + exe/Plugins.hs | 7 + haskell-language-server.cabal | 11 + nix/default.nix | 1 + .../src/Ide/Plugin/ExplicitImports.hs | 15 +- plugins/hls-refine-imports-plugin/LICENSE | 201 +++++++++++++ .../hls-refine-imports-plugin.cabal | 47 +++ .../src/Ide/Plugin/RefineImports.hs | 280 ++++++++++++++++++ .../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 + 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 + stack-8.8.4.yaml | 1 + stack.yaml | 1 + 29 files changed, 743 insertions(+), 5 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 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/.github/workflows/test.yml b/.github/workflows/test.yml index 4be3b656d..3b394f74d 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" diff --git a/cabal.project b/cabal.project index 07a87eb75..b82db73bc 100644 --- a/cabal.project +++ b/cabal.project @@ -12,6 +12,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 d6c37789f..d3c809c34 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -29,6 +29,10 @@ import Ide.Plugin.Eval as Eval import Ide.Plugin.ExplicitImports as ExplicitImports #endif +#if refineImports +import Ide.Plugin.RefineImports as RefineImports +#endif + #if retrie import Ide.Plugin.Retrie as Retrie #endif @@ -125,6 +129,9 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins #if importLens ExplicitImports.descriptor "importLens" : #endif +#if refineImports + RefineImports.descriptor "refineImports" : +#endif #if moduleName ModuleName.descriptor "moduleName" : #endif diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index e4df7a385..bbe15bd12 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 @@ -205,6 +210,11 @@ common importLens build-depends: hls-explicit-imports-plugin ^>= 1.0.0.1 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.1 @@ -279,6 +289,7 @@ executable haskell-language-server , haddockComments , eval , importLens + , refineImports , retrie , tactic , hlint diff --git a/nix/default.nix b/nix/default.nix index cf137eed7..5311d174f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,6 +36,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; 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 8b2c149d6..c9d067eb3 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/LICENSE b/plugins/hls-refine-imports-plugin/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /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 000000000..c79fd0605 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/hls-refine-imports-plugin.cabal @@ -0,0 +1,47 @@ +cabal-version: 2.2 +name: hls-refine-imports-plugin +version: 1.0.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 +extra-source-files: + LICENSE + +library + exposed-modules: Ide.Plugin.RefineImports + hs-source-dirs: src + 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 new file mode 100644 index 000000000..d5528e7e8 --- /dev/null +++ b/plugins/hls-refine-imports-plugin/src/Ide/Plugin/RefineImports.hs @@ -0,0 +1,280 @@ +{-# 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 (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) +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) + { pluginCommands = [refineImportCommand] + , pluginRules = refineImportsRule + , pluginHandlers = mconcat + [ -- This plugin provides code lenses + mkPluginHandler STextDocumentCodeLens lensProvider + -- This plugin provides code actions + , mkPluginHandler STextDocumentCodeAction codeActionProvider + ] + } + +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 _state (RefineImportCommandParams edit) = do + -- This command simply triggers a workspace edit! + _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + return (Right Null) + +lensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens +lensProvider + state -- ghcide state + 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 <- + runIde state $ useWithStale RefineImports nfp + case mbRefinedImports of + -- Implement the provider logic: + -- for every refined import, 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 []) + +-- | Provide one code action to refine all 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 $ CodeActionUnknown "quickfix.import.refine" + _command = Nothing + _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 [] + +-------------------------------------------------------------------------------- + +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) + 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 (ieName . unLoc) names + in flip Map.filter avails $ \a -> + any (`elem` importedNames) + $ 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 + +-------------------------------------------------------------------------------- + +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 = "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 + edit = WorkspaceEdit (Just editsMap) Nothing 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 {..} + +-------------------------------------------------------------------------------- + +-- | A helper to run ide actions +runIde :: IdeState -> Action a -> IO a +runIde = runAction "RefineImports" 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 000000000..a63f63354 --- /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 000000000..da94829c7 --- /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 000000000..a813ff528 --- /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 000000000..28434310d --- /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 000000000..afb002ca8 --- /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 000000000..7f61954f3 --- /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 000000000..6403caef3 --- /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 000000000..cb8193d35 --- /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 000000000..c743d4d11 --- /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 000000000..e25fa41be --- /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 000000000..0d1383c68 --- /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 diff --git a/stack-8.10.2.yaml b/stack-8.10.2.yaml index 3d26da055..7614ab2ec 100644 --- a/stack-8.10.2.yaml +++ b/stack-8.10.2.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.10.3.yaml b/stack-8.10.3.yaml index 629b0baaa..3018a1048 100644 --- a/stack-8.10.3.yaml +++ b/stack-8.10.3.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.10.4.yaml b/stack-8.10.4.yaml index 2c3edfaab..4979c786f 100644 --- a/stack-8.10.4.yaml +++ b/stack-8.10.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.4.yaml b/stack-8.6.4.yaml index 4b8561fd4..4e649c124 100644 --- a/stack-8.6.4.yaml +++ b/stack-8.6.4.yaml @@ -13,6 +13,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 2ba1400c2..8b475d205 100644 --- a/stack-8.6.5.yaml +++ b/stack-8.6.5.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.8.2.yaml b/stack-8.8.2.yaml index c329744e8..cb755c4d0 100644 --- a/stack-8.8.2.yaml +++ b/stack-8.8.2.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.8.3.yaml b/stack-8.8.3.yaml index ff4b393af..ecf355c26 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.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.8.4.yaml b/stack-8.8.4.yaml index 91d7a91ee..b7a50c443 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.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.yaml b/stack.yaml index 73678ad6a..c88b7d130 100644 --- a/stack.yaml +++ b/stack.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