hls-explicit-imports-plugin 1.0.2.0 → 1.1.0.0
raw patch · 3 files changed
+96/−18 lines, 3 filesdep ~ghcidedep ~hls-plugin-apinew-uploaderPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: ghcide, hls-plugin-api
API changes (from Hackage documentation)
+ Ide.Plugin.ExplicitImports: LogShake :: Log -> Log
+ Ide.Plugin.ExplicitImports: abbreviateImportTitle :: Text -> Text
+ Ide.Plugin.ExplicitImports: instance GHC.Show.Show Ide.Plugin.ExplicitImports.Log
+ Ide.Plugin.ExplicitImports: instance Prettyprinter.Internal.Pretty Ide.Plugin.ExplicitImports.Log
+ Ide.Plugin.ExplicitImports: newtype Log
- Ide.Plugin.ExplicitImports: descriptor :: PluginId -> PluginDescriptor IdeState
+ Ide.Plugin.ExplicitImports: descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
- Ide.Plugin.ExplicitImports: descriptorForModules :: (ModuleName -> Bool) -> PluginId -> PluginDescriptor IdeState
+ Ide.Plugin.ExplicitImports: descriptorForModules :: Recorder (WithPriority Log) -> (ModuleName -> Bool) -> PluginId -> PluginDescriptor IdeState
Files
- hls-explicit-imports-plugin.cabal +3/−3
- src/Ide/Plugin/ExplicitImports.hs +66/−13
- test/Main.hs +27/−2
hls-explicit-imports-plugin.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: hls-explicit-imports-plugin-version: 1.0.2.0+version: 1.1.0.0 synopsis: Explicit imports plugin for Haskell Language Server description: Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>@@ -24,9 +24,9 @@ , containers , deepseq , ghc- , ghcide ^>=1.6+ , ghcide ^>=1.7 , hls-graph- , hls-plugin-api ^>=1.3+ , hls-plugin-api ^>=1.4 , lsp , text , unordered-containers
src/Ide/Plugin/ExplicitImports.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}@@ -13,6 +14,8 @@ , descriptorForModules , extractMinimalImports , within+ , abbreviateImportTitle+ , Log(..) ) where import Control.DeepSeq@@ -26,11 +29,14 @@ import Data.Maybe (catMaybes, fromMaybe, isJust) import qualified Data.Text as T+import Data.String (fromString) import Development.IDE hiding (pluginHandlers, pluginRules) import Development.IDE.Core.PositionMapping+import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat import Development.IDE.Graph.Classes+import Development.IDE.Types.Logger as Logger (Pretty (pretty)) import GHC.Generics (Generic) import Ide.PluginUtils (mkLspCommand) import Ide.Types@@ -40,24 +46,33 @@ importCommandId :: CommandId importCommandId = "ImportLensCommand" +newtype Log+ = LogShake Shake.Log+ deriving Show++instance Pretty Log where+ pretty = \case+ LogShake log -> pretty log+ -- | The "main" function of a plugin-descriptor :: PluginId -> PluginDescriptor IdeState-descriptor =+descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState+descriptor recorder = -- (almost) no one wants to see an explicit import list for Prelude- descriptorForModules (/= moduleName pRELUDE)+ descriptorForModules recorder (/= moduleName pRELUDE) descriptorForModules- :: (ModuleName -> Bool)+ :: Recorder (WithPriority Log)+ -> (ModuleName -> Bool) -- ^ Predicate to select modules that will be annotated -> PluginId -> PluginDescriptor IdeState-descriptorForModules pred plId =+descriptorForModules recorder pred plId = (defaultPluginDescriptor plId) { -- This plugin provides a command handler pluginCommands = [importLensCommand], -- This plugin defines a new rule- pluginRules = minimalImportsRule,+ pluginRules = minimalImportsRule recorder, pluginHandlers = mconcat [ -- This plugin provides code lenses mkPluginHandler STextDocumentCodeLens $ lensProvider pred@@ -182,11 +197,11 @@ exportedModuleStrings ParsedModule{pm_parsed_source = L _ HsModule{..}} | Just export <- hsmodExports, exports <- unLoc export- = map prettyPrint exports+ = map (T.unpack . printOutputable) exports exportedModuleStrings _ = [] -minimalImportsRule :: Rules ()-minimalImportsRule = define $ \MinimalImports nfp -> do+minimalImportsRule :: Recorder (WithPriority Log) -> Rules ()+minimalImportsRule recorder = define (cmapWithPrio LogShake recorder) $ \MinimalImports nfp -> do -- Get the typechecking artifacts from the module tmr <- use TypeCheck nfp -- We also need a GHC session with all the dependencies@@ -195,7 +210,7 @@ (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr let importsMap = Map.fromList- [ (realSrcSpanStart l, T.pack (prettyPrint i))+ [ (realSrcSpanStart l, printOutputable i) | L (locA -> RealSrcSpan l _) i <- fromMaybe [] mbMinImports ] res =@@ -239,7 +254,6 @@ notExported [] _ = True notExported exports (L _ ImportDecl{ideclName = L _ name}) = not $ any (\e -> ("module " ++ moduleNameString name) == e) exports- notExported _ _ = False extractMinimalImports _ _ = return ([], Nothing) mkExplicitEdit :: (ModuleName -> Bool) -> PositionMapping -> LImportDecl GhcRn -> T.Text -> Maybe TextEdit@@ -256,12 +270,19 @@ | otherwise = Nothing +-- This number is somewhat arbitrarily chosen. Ideally the protocol would tell us these things,+-- but at the moment I don't believe we know it.+-- 80 columns is traditional, but Haskellers tend to use longer lines (citation needed) and it's+-- probably not too bad if the lens is a *bit* longer than normal lines.+maxColumns :: Int+maxColumns = 120+ -- | 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 importEdit@TextEdit {_range, _newText} = do- -- The title of the command is just the minimal explicit import decl- let title = _newText+ let+ title = abbreviateImportTitle _newText -- the code lens has no extra data _xdata = Nothing -- an edit that replaces the whole declaration with the explicit one@@ -273,6 +294,38 @@ _command = Just $ mkLspCommand pId importCommandId title _arguments -- create and return the code lens return $ Just CodeLens {..}++-- | The title of the command is ideally the minimal explicit import decl, but+-- we don't want to create a really massive code lens (and the decl can be extremely large!).+-- So we abbreviate it to fit a max column size, and indicate how many more items are in the list+-- after the abbreviation+abbreviateImportTitle :: T.Text -> T.Text+abbreviateImportTitle input =+ let+ -- For starters, we only want one line in the title+ oneLineText = T.unwords $ T.lines input+ -- Now, split at the max columns, leaving space for the summary text we're going to add+ -- (conservatively assuming we won't need to print a number larger than 100)+ (prefix, suffix) = T.splitAt (maxColumns - (T.length (summaryText 100))) oneLineText+ -- We also want to truncate the last item so we get a "clean" break, rather than half way through+ -- something. The conditional here is just because 'breakOnEnd' doesn't give us quite the right thing+ -- if there are actually no commas.+ (actualPrefix, extraSuffix) = if T.count "," prefix > 0 then T.breakOnEnd "," prefix else (prefix, "")+ actualSuffix = extraSuffix <> suffix++ -- The number of additional items is the number of commas+1+ numAdditionalItems = T.count "," actualSuffix + 1+ -- We want to make text like this: import Foo (AImport, BImport, ... (30 items))+ -- We also want it to look sensible if we end up splitting in the module name itself,+ summaryText n = " ... (" <> fromString (show n) <> " items)"+ -- so we only add a trailing paren if we've split in the export list+ suffixText = summaryText numAdditionalItems <> if T.count "(" prefix > 0 then ")" else ""+ title =+ -- If the original text fits, just use it+ if T.length oneLineText <= maxColumns+ then oneLineText+ else actualPrefix <> suffixText+ in title --------------------------------------------------------------------------------
test/Main.hs view
@@ -16,13 +16,15 @@ import Test.Hls explicitImportsPlugin :: PluginDescriptor IdeState-explicitImportsPlugin = ExplicitImports.descriptor "explicitImports"+explicitImportsPlugin = ExplicitImports.descriptor mempty "explicitImports" +longModule :: T.Text+longModule = "F" <> T.replicate 80 "o" main :: IO () main = defaultTestRunner $ testGroup- "Refine Imports"+ "Make imports explicit" [ codeActionGoldenTest "UsualCase" 3 0 , codeLensGoldenTest "UsualCase" 0 , testCase "No CodeAction when exported" $@@ -35,6 +37,29 @@ doc <- openDoc "Exported.hs" "haskell" lenses <- getCodeLenses doc liftIO $ lenses @?= []+ , testGroup "Title abbreviation"+ [ testCase "not abbreviated" $+ let i = "import " <> T.replicate 70 "F" <> " (Athing, Bthing, Cthing)"+ in ExplicitImports.abbreviateImportTitle i @?= i+ , testCase "abbreviated in module name" $+ let i = "import " <> T.replicate 120 "F" <> " (Athing, Bthing, Cthing)"+ o = "import " <> T.replicate 97 "F" <> " ... (3 items)"+ in ExplicitImports.abbreviateImportTitle i @?= o+ , testCase "abbreviated in import list" $+ let i = "import " <> T.replicate 78 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"+ o = "import " <> T.replicate 78 "F" <> " (Athing, Bthing, ... (3 items))"+ in ExplicitImports.abbreviateImportTitle i @?= o+ -- This one breaks earlier in the same import item, but still splits the list in the same place+ , testCase "abbreviated in import list (slightly shorter module)" $+ let i = "import " <> T.replicate 76 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"+ o = "import " <> T.replicate 76 "F" <> " (Athing, Bthing, ... (3 items))"+ in ExplicitImports.abbreviateImportTitle i @?= o+ -- This one breaks later in the same import item, but still splits the list in the same place+ , testCase "abbreviated in import list (slightly longer module)" $+ let i = "import " <> T.replicate 80 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"+ o = "import " <> T.replicate 80 "F" <> " (Athing, Bthing, ... (3 items))"+ in ExplicitImports.abbreviateImportTitle i @?= o+ ] ] -- code action tests