Skip to content

Commit 621c2bb

Browse files
authored
Abbreviate explicit import code lenses (#2769)
* Abbreviate explicit import code lenses The tests currently don't check anything about the titles, I'm unsure whether it's worth writing a test just for this. Fixes #2765. * Add tests for abbreviation and fix bugs * Fix a warning
1 parent f146ef0 commit 621c2bb

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Ide.Plugin.ExplicitImports
1414
, descriptorForModules
1515
, extractMinimalImports
1616
, within
17+
, abbreviateImportTitle
1718
, Log(..)
1819
) where
1920

@@ -28,6 +29,7 @@ import qualified Data.Map.Strict as Map
2829
import Data.Maybe (catMaybes, fromMaybe,
2930
isJust)
3031
import qualified Data.Text as T
32+
import Data.String (fromString)
3133
import Development.IDE hiding (pluginHandlers,
3234
pluginRules)
3335
import Development.IDE.Core.PositionMapping
@@ -252,7 +254,6 @@ extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do
252254
notExported [] _ = True
253255
notExported exports (L _ ImportDecl{ideclName = L _ name}) =
254256
not $ any (\e -> ("module " ++ moduleNameString name) == e) exports
255-
notExported _ _ = False
256257
extractMinimalImports _ _ = return ([], Nothing)
257258

258259
mkExplicitEdit :: (ModuleName -> Bool) -> PositionMapping -> LImportDecl GhcRn -> T.Text -> Maybe TextEdit
@@ -269,12 +270,19 @@ mkExplicitEdit pred posMapping (L (locA -> src) imp) explicit
269270
| otherwise =
270271
Nothing
271272

273+
-- This number is somewhat arbitrarily chosen. Ideally the protocol would tell us these things,
274+
-- but at the moment I don't believe we know it.
275+
-- 80 columns is traditional, but Haskellers tend to use longer lines (citation needed) and it's
276+
-- probably not too bad if the lens is a *bit* longer than normal lines.
277+
maxColumns :: Int
278+
maxColumns = 120
279+
272280
-- | Given an import declaration, generate a code lens unless it has an
273281
-- explicit import list or it's qualified
274282
generateLens :: PluginId -> Uri -> TextEdit -> IO (Maybe CodeLens)
275283
generateLens pId uri importEdit@TextEdit {_range, _newText} = do
276-
-- The title of the command is just the minimal explicit import decl
277-
let title = _newText
284+
let
285+
title = abbreviateImportTitle _newText
278286
-- the code lens has no extra data
279287
_xdata = Nothing
280288
-- an edit that replaces the whole declaration with the explicit one
@@ -287,6 +295,38 @@ generateLens pId uri importEdit@TextEdit {_range, _newText} = do
287295
-- create and return the code lens
288296
return $ Just CodeLens {..}
289297

298+
-- | The title of the command is ideally the minimal explicit import decl, but
299+
-- we don't want to create a really massive code lens (and the decl can be extremely large!).
300+
-- So we abbreviate it to fit a max column size, and indicate how many more items are in the list
301+
-- after the abbreviation
302+
abbreviateImportTitle :: T.Text -> T.Text
303+
abbreviateImportTitle input =
304+
let
305+
-- For starters, we only want one line in the title
306+
oneLineText = T.unwords $ T.lines input
307+
-- Now, split at the max columns, leaving space for the summary text we're going to add
308+
-- (conservatively assuming we won't need to print a number larger than 100)
309+
(prefix, suffix) = T.splitAt (maxColumns - (T.length (summaryText 100))) oneLineText
310+
-- We also want to truncate the last item so we get a "clean" break, rather than half way through
311+
-- something. The conditional here is just because 'breakOnEnd' doesn't give us quite the right thing
312+
-- if there are actually no commas.
313+
(actualPrefix, extraSuffix) = if T.count "," prefix > 0 then T.breakOnEnd "," prefix else (prefix, "")
314+
actualSuffix = extraSuffix <> suffix
315+
316+
-- The number of additional items is the number of commas+1
317+
numAdditionalItems = T.count "," actualSuffix + 1
318+
-- We want to make text like this: import Foo (AImport, BImport, ... (30 items))
319+
-- We also want it to look sensible if we end up splitting in the module name itself,
320+
summaryText n = " ... (" <> fromString (show n) <> " items)"
321+
-- so we only add a trailing paren if we've split in the export list
322+
suffixText = summaryText numAdditionalItems <> if T.count "(" prefix > 0 then ")" else ""
323+
title =
324+
-- If the original text fits, just use it
325+
if T.length oneLineText <= maxColumns
326+
then oneLineText
327+
else actualPrefix <> suffixText
328+
in title
329+
290330
--------------------------------------------------------------------------------
291331

292332
-- | A helper to run ide actions

plugins/hls-explicit-imports-plugin/test/Main.hs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import Test.Hls
1818
explicitImportsPlugin :: PluginDescriptor IdeState
1919
explicitImportsPlugin = ExplicitImports.descriptor mempty "explicitImports"
2020

21+
longModule :: T.Text
22+
longModule = "F" <> T.replicate 80 "o"
2123

2224
main :: IO ()
2325
main = defaultTestRunner $
2426
testGroup
25-
"Refine Imports"
27+
"Make imports explicit"
2628
[ codeActionGoldenTest "UsualCase" 3 0
2729
, codeLensGoldenTest "UsualCase" 0
2830
, testCase "No CodeAction when exported" $
@@ -35,6 +37,29 @@ main = defaultTestRunner $
3537
doc <- openDoc "Exported.hs" "haskell"
3638
lenses <- getCodeLenses doc
3739
liftIO $ lenses @?= []
40+
, testGroup "Title abbreviation"
41+
[ testCase "not abbreviated" $
42+
let i = "import " <> T.replicate 70 "F" <> " (Athing, Bthing, Cthing)"
43+
in ExplicitImports.abbreviateImportTitle i @?= i
44+
, testCase "abbreviated in module name" $
45+
let i = "import " <> T.replicate 120 "F" <> " (Athing, Bthing, Cthing)"
46+
o = "import " <> T.replicate 97 "F" <> " ... (3 items)"
47+
in ExplicitImports.abbreviateImportTitle i @?= o
48+
, testCase "abbreviated in import list" $
49+
let i = "import " <> T.replicate 78 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"
50+
o = "import " <> T.replicate 78 "F" <> " (Athing, Bthing, ... (3 items))"
51+
in ExplicitImports.abbreviateImportTitle i @?= o
52+
-- This one breaks earlier in the same import item, but still splits the list in the same place
53+
, testCase "abbreviated in import list (slightly shorter module)" $
54+
let i = "import " <> T.replicate 76 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"
55+
o = "import " <> T.replicate 76 "F" <> " (Athing, Bthing, ... (3 items))"
56+
in ExplicitImports.abbreviateImportTitle i @?= o
57+
-- This one breaks later in the same import item, but still splits the list in the same place
58+
, testCase "abbreviated in import list (slightly longer module)" $
59+
let i = "import " <> T.replicate 80 "F" <> " (Athing, Bthing, Cthing, Dthing, Ething)"
60+
o = "import " <> T.replicate 80 "F" <> " (Athing, Bthing, ... (3 items))"
61+
in ExplicitImports.abbreviateImportTitle i @?= o
62+
]
3863
]
3964

4065
-- code action tests

0 commit comments

Comments
 (0)