forked from haskell/haskell-language-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathExplicitImports.hs
265 lines (235 loc) · 10.3 KB
/
ExplicitImports.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
#include "ghc-api-version.h"
module Ide.Plugin.ExplicitImports
( descriptor
, extractMinimalImports
, within
) where
import Control.DeepSeq
import Control.Monad.IO.Class
import Data.Aeson (ToJSON (toJSON),
Value (Null))
import Data.Aeson.Types (FromJSON)
import qualified Data.HashMap.Strict as HashMap
import Data.IORef (readIORef)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe)
import qualified Data.Text as T
import Development.IDE
import Development.IDE.Core.PositionMapping
import Development.IDE.GHC.Compat
import Development.IDE.Graph.Classes
import GHC.Generics (Generic)
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)
import TcRnTypes (TcGblEnv (tcg_used_gres))
importCommandId :: CommandId
importCommandId = "ImportLensCommand"
-- | The "main" function of a plugin
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId =
(defaultPluginDescriptor plId)
{
-- This plugin provides a command handler
pluginCommands = [importLensCommand],
-- This plugin defines a new rule
pluginRules = minimalImportsRule,
pluginHandlers = mconcat
[ -- This plugin provides code lenses
mkPluginHandler STextDocumentCodeLens lensProvider
-- This plugin provides code actions
, mkPluginHandler STextDocumentCodeAction codeActionProvider
]
}
-- | The command descriptor
importLensCommand :: PluginCommand IdeState
importLensCommand =
PluginCommand importCommandId "Explicit import command" runImportCommand
-- | The type of the parameters accepted by our command
data ImportCommandParams = ImportCommandParams WorkspaceEdit
deriving (Generic)
deriving anyclass (FromJSON, ToJSON)
-- | The actual command handler
runImportCommand :: CommandFunction IdeState ImportCommandParams
runImportCommand _state (ImportCommandParams edit) = do
-- This command simply triggers a workspace edit!
_ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ())
return (Right Null)
-- | For every implicit import statement, return a code lens of the corresponding explicit import
-- Example - for the module below:
--
-- > import Data.List
-- >
-- > f = intercalate " " . sortBy length
--
-- the provider should produce one code lens associated to the import statement:
--
-- > import Data.List (intercalate, sortBy)
lensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens
lensProvider
state -- ghcide state, used to retrieve typechecking artifacts
pId -- plugin Id
CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}}
-- VSCode uses URIs instead of file paths
-- haskell-lsp provides conversion functions
| Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = liftIO $
do
mbMinImports <- runAction "" state $ useWithStale MinimalImports nfp
case mbMinImports of
-- Implement the provider logic:
-- for every import, if it's lacking a explicit list, generate a code lens
Just (MinimalImportsResult minImports, posMapping) -> do
commands <-
sequence
[ generateLens pId _uri edit
| (imp, Just minImport) <- minImports,
Just edit <- [mkExplicitEdit posMapping imp minImport]
]
return $ Right (List $ catMaybes commands)
_ ->
return $ Right (List [])
| otherwise =
return $ Right (List [])
-- | If there are any implicit imports, provide one code action to turn them all
-- into explicit 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
minImports <- runAction "MinimalImports" ideState $ use MinimalImports nfp
let edits =
[ e
| (imp, Just explicit) <-
maybe [] getMinimalImportsResult minImports,
Just e <- [mkExplicitEdit zeroMapping imp explicit]
]
caExplicitImports = InR CodeAction {..}
_title = "Make all imports explicit"
_kind = Just CodeActionQuickFix
_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 MinimalImports = MinimalImports
deriving (Show, Generic, Eq, Ord)
instance Hashable MinimalImports
instance NFData MinimalImports
instance Binary MinimalImports
type instance RuleResult MinimalImports = MinimalImportsResult
newtype MinimalImportsResult = MinimalImportsResult
{getMinimalImportsResult :: [(LImportDecl GhcRn, Maybe T.Text)]}
instance Show MinimalImportsResult where show _ = "<minimalImportsResult>"
instance NFData MinimalImportsResult where rnf = rwhnf
minimalImportsRule :: Rules ()
minimalImportsRule = define $ \MinimalImports 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
-- Use the GHC api to extract the "minimal" imports
(imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr
let importsMap =
Map.fromList
[ (srcSpanStart l, T.pack (prettyPrint i))
| L l i <- fromMaybe [] mbMinImports
]
res =
[ (i, Map.lookup (srcSpanStart (getLoc i)) importsMap)
| i <- imports
]
return ([], MinimalImportsResult res <$ mbMinImports)
--------------------------------------------------------------------------------
-- | Use the ghc api to extract a minimal, explicit set of imports for this module
extractMinimalImports ::
Maybe HscEnvEq ->
Maybe TcModuleResult ->
IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn])
extractMinimalImports (Just hsc) (Just TcModuleResult {..}) = do
-- extract the original imports and the typechecking environment
let tcEnv = tmrTypechecked
(_, imports, _, _) = tmrRenamed
ParsedModule {pm_parsed_source = L loc _} = tmrParsed
span = fromMaybe (error "expected real") $ realSpan loc
-- GHC is secretly full of mutable state
gblElts <- readIORef (tcg_used_gres tcEnv)
-- 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
-- return both the original imports and the computed minimal ones
return (imports, minimalImports)
extractMinimalImports _ _ = return ([], Nothing)
mkExplicitEdit :: PositionMapping -> LImportDecl pass -> T.Text -> Maybe TextEdit
mkExplicitEdit posMapping (L src imp) explicit
-- Explicit import list case
| ImportDecl {ideclHiding = Just (False, _)} <- imp =
Nothing
| not (isQualifiedImport imp),
RealSrcSpan l <- src,
L _ mn <- ideclName imp,
-- (almost) no one wants to see an explicit 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 importEdit@TextEdit {_range, _newText} = do
-- The title of the command is just the minimal explicit import decl
let title = _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 [importEdit])]
-- the command argument is simply the edit
_arguments = Just [toJSON $ ImportCommandParams edit]
-- create the command
_command = Just $ mkLspCommand pId importCommandId 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 "importLens"
within :: Range -> SrcSpan -> Bool
within (Range start end) span =
isInsideSrcSpan start span || isInsideSrcSpan end span