Skip to content

Commit 1e43820

Browse files
Kobayashipepeiborramergify[bot]
authored andcommitted
fix new import position (haskell#2981)
* haskell#2414 fix new import position * fix auto import for ghc version < 9.2 * re-fix it for ghc-9.2 * handle comments * add more comments * reword comments of findPositionNoImports Co-authored-by: Pepe Iborra <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent fc1e503 commit 1e43820

File tree

5 files changed

+193
-66
lines changed

5 files changed

+193
-66
lines changed

ghcide/src/Development/IDE/Plugin/CodeAction.hs

Lines changed: 128 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ module Development.IDE.Plugin.CodeAction
1919

2020
import Control.Applicative ((<|>))
2121
import Control.Arrow (second,
22-
(>>>),
23-
(&&&))
22+
(&&&),
23+
(>>>))
2424
import Control.Concurrent.STM.Stats (atomically)
25-
import Control.Monad (guard, join,
26-
msum)
25+
import Control.Monad (guard, join)
2726
import Control.Monad.IO.Class
2827
import Data.Char
2928
import qualified Data.DList as DL
@@ -34,7 +33,7 @@ import qualified Data.HashSet as Set
3433
import Data.List.Extra
3534
import Data.List.NonEmpty (NonEmpty ((:|)))
3635
import qualified Data.List.NonEmpty as NE
37-
import qualified Data.Map as M
36+
import qualified Data.Map.Strict as M
3837
import Data.Maybe
3938
import Data.Ord (comparing)
4039
import qualified Data.Rope.UTF16 as Rope
@@ -47,7 +46,6 @@ import Development.IDE.Core.Service
4746
import Development.IDE.GHC.Compat
4847
import Development.IDE.GHC.Compat.Util
4948
import Development.IDE.GHC.Error
50-
import Development.IDE.GHC.ExactPrint
5149
import Development.IDE.GHC.Util (printOutputable,
5250
printRdrName,
5351
traceAst)
@@ -80,6 +78,25 @@ import Language.LSP.Types (CodeAction (
8078
import Language.LSP.VFS
8179
import Text.Regex.TDFA (mrAfter,
8280
(=~), (=~~))
81+
#if MIN_VERSION_ghc(9,2,0)
82+
import GHC (AddEpAnn (AddEpAnn),
83+
Anchor (anchor_op),
84+
AnchorOperation (..),
85+
AnnsModule (am_main),
86+
DeltaPos (..),
87+
EpAnn (..),
88+
EpaLocation (..),
89+
LEpaComment,
90+
LocatedA)
91+
92+
import Control.Monad (msum)
93+
#else
94+
import Language.Haskell.GHC.ExactPrint.Types (Annotation (annsDP),
95+
DeltaPos,
96+
KeywordId (G),
97+
deltaRow,
98+
mkAnnKey)
99+
#endif
83100

84101
-------------------------------------------------------------------------------------------------
85102

@@ -227,10 +244,8 @@ findInstanceHead df instanceHead decls =
227244

228245
#if MIN_VERSION_ghc(9,2,0)
229246
findDeclContainingLoc :: Foldable t => Position -> t (GenLocated (SrcSpanAnn' a) e) -> Maybe (GenLocated (SrcSpanAnn' a) e)
230-
#elif MIN_VERSION_ghc(8,10,0)
231-
findDeclContainingLoc :: Foldable t => Position -> t (GenLocated SrcSpan e) -> Maybe (GenLocated SrcSpan e)
232247
#else
233-
-- TODO populate this type signature for GHC versions <8.10
248+
findDeclContainingLoc :: Foldable t => Position -> t (GenLocated SrcSpan e) -> Maybe (GenLocated SrcSpan e)
234249
#endif
235250
findDeclContainingLoc loc = find (\(L l _) -> loc `isInsideSrcSpan` locA l)
236251

@@ -243,8 +258,8 @@ findDeclContainingLoc loc = find (\(L l _) -> loc `isInsideSrcSpan` locA l)
243258
-- imported from ‘Data.ByteString’ at B.hs:6:1-22
244259
-- imported from ‘Data.ByteString.Lazy’ at B.hs:8:1-27
245260
-- imported from ‘Data.Text’ at B.hs:7:1-16
246-
suggestHideShadow :: ParsedSource -> T.Text -> Maybe TcModuleResult -> Maybe HieAstResult -> Diagnostic -> [(T.Text, [Either TextEdit Rewrite])]
247-
suggestHideShadow ps@(L _ HsModule {hsmodImports}) fileContents mTcM mHar Diagnostic {_message, _range}
261+
suggestHideShadow :: Annotated ParsedSource -> T.Text -> Maybe TcModuleResult -> Maybe HieAstResult -> Diagnostic -> [(T.Text, [Either TextEdit Rewrite])]
262+
suggestHideShadow ps fileContents mTcM mHar Diagnostic {_message, _range}
248263
| Just [identifier, modName, s] <-
249264
matchRegexUnifySpaces
250265
_message
@@ -261,6 +276,8 @@ suggestHideShadow ps@(L _ HsModule {hsmodImports}) fileContents mTcM mHar Diagno
261276
result <> [hideAll]
262277
| otherwise = []
263278
where
279+
L _ HsModule {hsmodImports} = astA ps
280+
264281
suggests identifier modName s
265282
| Just tcM <- mTcM,
266283
Just har <- mHar,
@@ -940,11 +957,11 @@ isPreludeImplicit = xopt Lang.ImplicitPrelude
940957
suggestImportDisambiguation ::
941958
DynFlags ->
942959
Maybe T.Text ->
943-
ParsedSource ->
960+
Annotated ParsedSource ->
944961
T.Text ->
945962
Diagnostic ->
946963
[(T.Text, [Either TextEdit Rewrite])]
947-
suggestImportDisambiguation df (Just txt) ps@(L _ HsModule {hsmodImports}) fileContents diag@Diagnostic {..}
964+
suggestImportDisambiguation df (Just txt) ps fileContents diag@Diagnostic {..}
948965
| Just [ambiguous] <-
949966
matchRegexUnifySpaces
950967
_message
@@ -956,6 +973,8 @@ suggestImportDisambiguation df (Just txt) ps@(L _ HsModule {hsmodImports}) fileC
956973
suggestions ambiguous modules (isJust local)
957974
| otherwise = []
958975
where
976+
L _ HsModule {hsmodImports} = astA ps
977+
959978
locDic =
960979
fmap (NE.fromList . DL.toList) $
961980
Map.fromListWith (<>) $
@@ -1048,21 +1067,21 @@ targetModuleName (ExistingImp _) =
10481067
error "Cannot happen!"
10491068

10501069
disambiguateSymbol ::
1051-
ParsedSource ->
1070+
Annotated ParsedSource ->
10521071
T.Text ->
10531072
Diagnostic ->
10541073
T.Text ->
10551074
HidingMode ->
10561075
[Either TextEdit Rewrite]
1057-
disambiguateSymbol pm fileContents Diagnostic {..} (T.unpack -> symbol) = \case
1076+
disambiguateSymbol ps fileContents Diagnostic {..} (T.unpack -> symbol) = \case
10581077
(HideOthers hiddens0) ->
10591078
[ Right $ hideSymbol symbol idecl
10601079
| ExistingImp idecls <- hiddens0
10611080
, idecl <- NE.toList idecls
10621081
]
10631082
++ mconcat
10641083
[ if null imps
1065-
then maybeToList $ Left . snd <$> newImportToEdit (hideImplicitPreludeSymbol $ T.pack symbol) pm fileContents
1084+
then maybeToList $ Left . snd <$> newImportToEdit (hideImplicitPreludeSymbol $ T.pack symbol) ps fileContents
10661085
else Right . hideSymbol symbol <$> imps
10671086
| ImplicitPrelude imps <- hiddens0
10681087
]
@@ -1292,7 +1311,7 @@ removeRedundantConstraints df (L _ HsModule {hsmodDecls}) Diagnostic{..}
12921311

12931312
-------------------------------------------------------------------------------------------------
12941313

1295-
suggestNewOrExtendImportForClassMethod :: ExportsMap -> ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, [Either TextEdit Rewrite])]
1314+
suggestNewOrExtendImportForClassMethod :: ExportsMap -> Annotated ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, [Either TextEdit Rewrite])]
12961315
suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnostic {_message}
12971316
| Just [methodName, className] <-
12981317
matchRegexUnifySpaces
@@ -1306,7 +1325,7 @@ suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnos
13061325
where
13071326
suggest identInfo@IdentInfo {moduleNameText}
13081327
| importStyle <- NE.toList $ importStyles identInfo,
1309-
mImportDecl <- findImportDeclByModuleName (hsmodImports $ unLoc ps) (T.unpack moduleNameText) =
1328+
mImportDecl <- findImportDeclByModuleName (hsmodImports . unLoc . astA $ ps) (T.unpack moduleNameText) =
13101329
case mImportDecl of
13111330
-- extend
13121331
Just decl ->
@@ -1328,8 +1347,8 @@ suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnos
13281347
<> [(quickFixImportKind "new.all", newImportAll moduleNameText)]
13291348
| otherwise -> []
13301349

1331-
suggestNewImport :: ExportsMap -> ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)]
1332-
suggestNewImport packageExportsMap ps@(L _ HsModule {..}) fileContents Diagnostic{_message}
1350+
suggestNewImport :: ExportsMap -> Annotated ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)]
1351+
suggestNewImport packageExportsMap ps fileContents Diagnostic{_message}
13331352
| msg <- unifySpaces _message
13341353
, Just thingMissing <- extractNotInScopeName msg
13351354
, qual <- extractQualifiedModuleName msg
@@ -1344,6 +1363,8 @@ suggestNewImport packageExportsMap ps@(L _ HsModule {..}) fileContents Diagnosti
13441363
= sortOn fst3 [(imp, kind, TextEdit range (imp <> "\n" <> T.replicate indent " "))
13451364
| (kind, unNewImport -> imp) <- constructNewImportSuggestions packageExportsMap (qual <|> qual', thingMissing) extendImportSuggestions
13461365
]
1366+
where
1367+
L _ HsModule {..} = astA ps
13471368
suggestNewImport _ _ _ _ = []
13481369

13491370
constructNewImportSuggestions
@@ -1371,7 +1392,7 @@ constructNewImportSuggestions exportsMap (qual, thingMissing) notTheseModules =
13711392
newtype NewImport = NewImport {unNewImport :: T.Text}
13721393
deriving (Show, Eq, Ord)
13731394

1374-
newImportToEdit :: NewImport -> ParsedSource -> T.Text -> Maybe (T.Text, TextEdit)
1395+
newImportToEdit :: NewImport -> Annotated ParsedSource -> T.Text -> Maybe (T.Text, TextEdit)
13751396
newImportToEdit (unNewImport -> imp) ps fileContents
13761397
| Just (range, indent) <- newImportInsertRange ps fileContents
13771398
= Just (imp, TextEdit range (imp <> "\n" <> T.replicate indent " "))
@@ -1385,35 +1406,105 @@ newImportToEdit (unNewImport -> imp) ps fileContents
13851406
-- * If the file has neither existing imports nor a module declaration,
13861407
-- the import will be inserted at line zero if there are no pragmas,
13871408
-- * otherwise inserted one line after the last file-header pragma
1388-
newImportInsertRange :: ParsedSource -> T.Text -> Maybe (Range, Int)
1389-
newImportInsertRange (L _ HsModule {..}) fileContents
1409+
newImportInsertRange :: Annotated ParsedSource -> T.Text -> Maybe (Range, Int)
1410+
newImportInsertRange ps fileContents
13901411
| Just ((l, c), col) <- case hsmodImports of
1391-
[] -> findPositionNoImports (fmap reLoc hsmodName) (fmap reLoc hsmodExports) fileContents
1392-
_ -> findPositionFromImportsOrModuleDecl (map reLoc hsmodImports) last True
1412+
-- When there is no existing imports, we only cares about the line number, setting column and indent to zero.
1413+
[] -> (\line -> ((line, 0), 0)) <$> findPositionNoImports ps fileContents
1414+
_ -> findPositionFromImports (map reLoc hsmodImports) last
13931415
, let insertPos = Position (fromIntegral l) (fromIntegral c)
13941416
= Just (Range insertPos insertPos, col)
13951417
| otherwise = Nothing
1418+
where
1419+
L _ HsModule {..} = astA ps
1420+
1421+
-- | Find the position for a new import when there isn't an existing one.
1422+
-- * If there is a module declaration, a new import should be inserted under the module declaration (including exports list)
1423+
-- * Otherwise, a new import should be inserted after any file-header pragma.
1424+
findPositionNoImports :: Annotated ParsedSource -> T.Text -> Maybe Int
1425+
findPositionNoImports ps fileContents =
1426+
maybe (Just (findNextPragmaPosition fileContents)) (findPositionAfterModuleName ps) hsmodName
1427+
where
1428+
L _ HsModule {..} = astA ps
13961429

1397-
-- | Insert the import under the Module declaration exports if they exist, otherwise just under the module declaration.
1398-
-- If no module declaration exists, then no exports will exist either, in that case
1399-
-- insert the import after any file-header pragmas or at position zero if there are no pragmas
1400-
findPositionNoImports :: Maybe (Located ModuleName) -> Maybe (Located [LIE name]) -> T.Text -> Maybe ((Int, Int), Int)
1401-
findPositionNoImports Nothing _ fileContents = findNextPragmaPosition fileContents
1402-
findPositionNoImports _ (Just hsmodExports) _ = findPositionFromImportsOrModuleDecl hsmodExports id False
1403-
findPositionNoImports (Just hsmodName) _ _ = findPositionFromImportsOrModuleDecl hsmodName id False
1430+
-- | find line number right after module ... where
1431+
findPositionAfterModuleName :: Annotated ParsedSource
1432+
#if MIN_VERSION_ghc(9,2,0)
1433+
-> LocatedA ModuleName
1434+
#else
1435+
-> Located ModuleName
1436+
#endif
1437+
-> Maybe Int
1438+
findPositionAfterModuleName ps hsmodName' = do
1439+
-- Note that 'where' keyword and comments are not part of the AST. They belongs to
1440+
-- the exact-print information. To locate it, we need to find the previous AST node,
1441+
-- calculate the gap between it and 'where', then add them up to produce the absolute
1442+
-- position of 'where'.
1443+
1444+
lineOffset <- whereKeywordLineOffset -- Calculate the gap before 'where' keyword.
1445+
case prevSrcSpan of
1446+
UnhelpfulSpan _ -> Nothing
1447+
(RealSrcSpan prevSrcSpan' _) ->
1448+
-- add them up produce the absolute location of 'where' keyword
1449+
Just $ srcLocLine (realSrcSpanEnd prevSrcSpan') + lineOffset
1450+
where
1451+
L _ HsModule {..} = astA ps
1452+
1453+
-- The last AST node before 'where' keyword. Might be module name or export list.
1454+
prevSrcSpan = maybe (getLoc hsmodName') getLoc hsmodExports
1455+
1456+
-- The relative position of 'where' keyword (in lines, relative to the previous AST node).
1457+
-- The exact-print API changed a lot in ghc-9.2, so we need to handle it seperately for different compiler versions.
1458+
whereKeywordLineOffset :: Maybe Int
1459+
#if MIN_VERSION_ghc(9,2,0)
1460+
whereKeywordLineOffset = case hsmodAnn of
1461+
EpAnn _ annsModule _ -> do
1462+
-- Find the first 'where'
1463+
whereLocation <- fmap NE.head . NE.nonEmpty . mapMaybe filterWhere . am_main $ annsModule
1464+
epaLocationToLine whereLocation
1465+
EpAnnNotUsed -> Nothing
1466+
filterWhere (AddEpAnn AnnWhere loc) = Just loc
1467+
filterWhere _ = Nothing
1468+
1469+
epaLocationToLine :: EpaLocation -> Maybe Int
1470+
epaLocationToLine (EpaSpan sp) = Just . srcLocLine . realSrcSpanEnd $ sp
1471+
epaLocationToLine (EpaDelta (SameLine _) priorComments) = Just $ sumCommentsOffset priorComments
1472+
-- 'priorComments' contains the comments right before the current EpaLocation
1473+
-- Summing line offset of priorComments is necessary, as 'line' is the gap between the last comment and
1474+
-- the current AST node
1475+
epaLocationToLine (EpaDelta (DifferentLine line _) priorComments) = Just (line + sumCommentsOffset priorComments)
1476+
1477+
sumCommentsOffset :: [LEpaComment] -> Int
1478+
sumCommentsOffset = sum . fmap (\(L anchor _) -> anchorOpLine (anchor_op anchor))
1479+
1480+
anchorOpLine :: AnchorOperation -> Int
1481+
anchorOpLine UnchangedAnchor = 0
1482+
anchorOpLine (MovedAnchor (SameLine _)) = 0
1483+
anchorOpLine (MovedAnchor (DifferentLine line _)) = line
1484+
#else
1485+
whereKeywordLineOffset = do
1486+
ann <- annsA ps M.!? mkAnnKey (astA ps)
1487+
deltaPos <- fmap NE.head . NE.nonEmpty .mapMaybe filterWhere $ annsDP ann
1488+
pure $ deltaRow deltaPos
1489+
1490+
-- Before ghc 9.2, DeltaPos doesn't take comment into acccount, so we don't need to sum line offset of comments.
1491+
filterWhere :: (KeywordId, DeltaPos) -> Maybe DeltaPos
1492+
filterWhere (keywordId, deltaPos) =
1493+
if keywordId == G AnnWhere then Just deltaPos else Nothing
1494+
#endif
14041495

1405-
findPositionFromImportsOrModuleDecl :: HasSrcSpan a => t -> (t -> a) -> Bool -> Maybe ((Int, Int), Int)
1406-
findPositionFromImportsOrModuleDecl hsField f hasImports = case getLoc (f hsField) of
1496+
findPositionFromImports :: HasSrcSpan a => t -> (t -> a) -> Maybe ((Int, Int), Int)
1497+
findPositionFromImports hsField f = case getLoc (f hsField) of
14071498
RealSrcSpan s _ ->
14081499
let col = calcCol s
14091500
in Just ((srcLocLine (realSrcSpanEnd s), col), col)
14101501
_ -> Nothing
1411-
where calcCol s = if hasImports then srcLocCol (realSrcSpanStart s) - 1 else 0
1502+
where calcCol s = srcLocCol (realSrcSpanStart s) - 1
14121503

14131504
-- | Find the position one after the last file-header pragma
14141505
-- Defaults to zero if there are no pragmas in file
1415-
findNextPragmaPosition :: T.Text -> Maybe ((Int, Int), Int)
1416-
findNextPragmaPosition contents = Just ((lineNumber, 0), 0)
1506+
findNextPragmaPosition :: T.Text -> Int
1507+
findNextPragmaPosition contents = lineNumber
14171508
where
14181509
lineNumber = afterLangPragma . afterOptsGhc $ afterShebang
14191510
afterLangPragma = afterPragma "LANGUAGE" contents'

ghcide/src/Development/IDE/Plugin/Completions.hs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,7 @@ extendImportHandler' ideState ExtendImport {..}
250250
it = case thingParent of
251251
Nothing -> newThing
252252
Just p -> p <> "(" <> newThing <> ")"
253-
t <- liftMaybe $ snd <$> newImportToEdit
254-
n
255-
(astA ps)
256-
(fromMaybe "" contents)
253+
t <- liftMaybe $ snd <$> newImportToEdit n ps (fromMaybe "" contents)
257254
return (nfp, WorkspaceEdit {_changes=Just (fromList [(doc,List [t])]), _documentChanges=Nothing, _changeAnnotations=Nothing})
258255
| otherwise =
259256
mzero
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Asdf
2+
(f
3+
, where')
4+
-- hello
5+
-- world
6+
7+
where
8+
import Data.Int
9+
10+
11+
12+
f :: Int64 -> Int64
13+
f = id'
14+
where id' = id
15+
16+
g :: Int -> Int
17+
g = id
18+
19+
where' :: Int -> Int
20+
where' = id
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Asdf
2+
(f
3+
, where')
4+
-- hello
5+
-- world
6+
7+
where
8+
9+
10+
11+
f :: Int64 -> Int64
12+
f = id'
13+
where id' = id
14+
15+
g :: Int -> Int
16+
g = id
17+
18+
where' :: Int -> Int
19+
where' = id

0 commit comments

Comments
 (0)