packages feed

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 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