-
-
Notifications
You must be signed in to change notification settings - Fork 435
Expand file tree
/
Copy pathHandlers.hs
More file actions
699 lines (619 loc) · 29.6 KB
/
Handlers.hs
File metadata and controls
699 lines (619 loc) · 29.6 KB
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
-- | A plugin inspired by the REPLoid feature of
-- [Dante](https://github.com/jyp/dante),
-- [Haddock examples and properties](https://haskell-haddock.readthedocs.io/latest/markup.html#examples),
-- and [Doctest](https://hackage.haskell.org/package/doctest).
module Ide.Plugin.Eval.Handlers (
codeAction,
codeLens,
evalCommand,
) where
import Control.Applicative (Alternative ((<|>)))
import Control.Arrow (second)
import Control.Exception (bracket_)
import qualified Control.Exception as E
import Control.Lens (ix, (%~), (^.))
import Control.Monad (guard, void,
when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans.Except (ExceptT (..),
runExceptT)
import Data.Aeson (toJSON)
import Data.Char (isSpace)
import Data.Foldable (toList)
import Data.List (dropWhileEnd,
find,
intercalate,
intersperse)
import qualified Data.Map as Map
import Data.Maybe (catMaybes)
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Utf16.Rope.Mixed as Rope
import Development.IDE.Core.FileStore (getUriContents, setSomethingModified)
import Development.IDE.Core.Rules (IdeState,
runAction)
import Development.IDE.Core.Shake (use_, uses_, VFSModified (VFSUnmodified), useWithSeparateFingerprintRule_)
import Development.IDE.GHC.Compat hiding (typeKind,
unitState)
import Development.IDE.GHC.Compat.Util (OverridingBool (..))
import Development.IDE.GHC.Util (evalGhcEnv,
modifyDynFlags)
import Development.IDE.Import.DependencyInformation (transitiveDeps,
transitiveModuleDeps)
import Development.IDE.Types.Location (toNormalizedFilePath')
import GHC (ClsInst,
ExecOptions (execLineNumber, execSourceFile),
FamInst,
GhcMonad,
NamedThing (getName),
defaultFixity,
execOptions,
exprType,
getInfo,
getInteractiveDynFlags,
isImport, isStmt,
parseName,
pprFamInst,
pprInstance,
typeKind)
import Development.IDE.Core.RuleTypes (GetLinkable (GetLinkable),
GetModSummary (GetModSummary),
GetModuleGraphTransDepsFingerprints (GetModuleGraphTransDepsFingerprints),
GhcSessionDeps (GhcSessionDeps),
ModSummaryResult (msrModSummary),
LinkableResult (linkableHomeMod),
TypeCheck (..),
tmrTypechecked, GetModuleGraphTransDepsFingerprints(..), GetModuleGraph(..))
import qualified Development.IDE.GHC.Compat.Core as Compat (InteractiveImport (IIModule))
import qualified Development.IDE.GHC.Compat.Core as SrcLoc (unLoc)
import Development.IDE.Types.HscEnvEq (HscEnvEq (hscEnv))
import qualified GHC.LanguageExtensions.Type as LangExt (Extension (..))
import Data.List.Extra (unsnoc)
import Development.IDE.Core.PluginUtils
import Development.IDE.Types.Shake (toKey)
import GHC.Types.SrcLoc (UnhelpfulSpanReason (UnhelpfulInteractive))
import Ide.Logger (Priority (..),
Recorder,
WithPriority,
logWith)
import Ide.Plugin.Error (PluginError (PluginInternalError),
handleMaybeM)
import Ide.Plugin.Eval.Code (Statement,
asStatements,
myExecStmt,
propSetup,
resultRange,
testCheck,
testRanges)
import Ide.Plugin.Eval.Config (EvalConfig (..),
getEvalConfig)
import Ide.Plugin.Eval.GHC (addImport,
addPackages,
hasPackage,
setSessionAndInteractiveDynFlags)
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
import Ide.Plugin.Eval.Parse.Option (parseSetFlags)
import Ide.Plugin.Eval.Rules (queueForEvaluation,
unqueueForEvaluation)
import Ide.Plugin.Eval.Types
import Ide.Plugin.Eval.Util (gStrictTry,
isLiterate,
prettyWarnings,
response', timed)
import Ide.Types
import qualified Language.LSP.Protocol.Lens as L
import Language.LSP.Protocol.Message
import Language.LSP.Protocol.Types
import Language.LSP.Server
#if MIN_VERSION_ghc(9,11,0)
import GHC.Unit.Module.ModIface (IfaceTopEnv (..))
#endif
codeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCodeAction
codeAction recorder st plId CodeActionParams{_textDocument,_range} = do
rangeCommands <- mkRangeCommands recorder st plId _textDocument
pure
$ InL
[ InL command
| (testRange, command) <- rangeCommands
, _range `isSubrangeOf` testRange
]
{- | Code Lens provider
NOTE: Invoked every time the document is modified, not just when the document is saved.
-}
codeLens :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCodeLens
codeLens recorder st plId CodeLensParams{_textDocument} = do
rangeCommands <- mkRangeCommands recorder st plId _textDocument
pure
$ InL
[ CodeLens range (Just command) Nothing
| (range, command) <- rangeCommands
]
mkRangeCommands :: Recorder (WithPriority Log) -> IdeState -> PluginId -> TextDocumentIdentifier -> ExceptT PluginError (HandlerM Config) [(Range, Command)]
mkRangeCommands recorder st plId textDocument =
let dbg = logWith recorder Debug
perf = timed (\lbl duration -> dbg $ LogExecutionTime lbl duration)
in perf "evalMkRangeCommands" $
do
let TextDocumentIdentifier uri = textDocument
fp <- uriToFilePathE uri
let nfp = toNormalizedFilePath' fp
isLHS = isLiterate fp
dbg $ LogCodeLensFp fp
(comments, _) <-
runActionE "eval.GetParsedModuleWithComments" st $ useWithStaleE GetEvalComments nfp
dbg $ LogCodeLensComments comments
-- Extract tests from source code
let Sections{..} = commentsToSections isLHS comments
tests = testsBySection nonSetupSections
cmd = mkLspCommand plId evalCommandName "Evaluate=..." (Just [])
let rangeCommands =
[ (testRange, cmd')
| (section, ident, test) <- tests
, let (testRange, resultRange) = testRanges test
args = EvalParams (setupSections ++ [section]) textDocument ident
cmd' =
(cmd :: Command)
{ _arguments = Just [toJSON args]
, _title =
if trivial resultRange
then "Evaluate..."
else "Refresh..."
}
]
perf "tests" $
dbg $ LogTests
(length tests)
(length nonSetupSections)
(length setupSections)
(length rangeCommands)
pure rangeCommands
where
trivial (Range p p') = p == p'
evalCommandName :: CommandId
evalCommandName = "evalCommand"
evalCommand :: Recorder (WithPriority Log) -> PluginId -> PluginCommand IdeState
evalCommand recorder plId = PluginCommand evalCommandName "evaluate" (runEvalCmd recorder plId)
type EvalId = Int
runEvalCmd :: Recorder (WithPriority Log) -> PluginId -> CommandFunction IdeState EvalParams
runEvalCmd recorder plId st mtoken EvalParams{..} =
let dbg = logWith recorder Debug
perf = timed (\lbl duration -> dbg $ LogExecutionTime lbl duration)
cmd :: ExceptT PluginError (HandlerM Config) WorkspaceEdit
cmd = do
let tests = map (\(a,_,b) -> (a,b)) $ testsBySection sections
let TextDocumentIdentifier{_uri} = module_
fp <- uriToFilePathE _uri
let nfp = toNormalizedFilePath' fp
mdlText <- moduleText st _uri
-- enable codegen for the module which we need to evaluate.
final_hscEnv <- liftIO $ bracket_
(setSomethingModified VFSUnmodified st "Eval" $ do
queueForEvaluation st nfp
return [toKey IsEvaluating nfp]
)
(setSomethingModified VFSUnmodified st "Eval" $ do
unqueueForEvaluation st nfp
return [toKey IsEvaluating nfp]
)
(initialiseSessionForEval (needsQuickCheck tests) st nfp)
evalCfg <- liftIO $ runAction "eval: config" st $ getEvalConfig plId
-- Perform the evaluation of the command
edits <-
perf "edits" $
liftIO $
evalGhcEnv final_hscEnv $ do
runTests recorder evalCfg fp tests
let workspaceEditsMap = Map.singleton _uri (addFinalReturn mdlText edits)
let workspaceEdits = WorkspaceEdit (Just workspaceEditsMap) Nothing Nothing
return workspaceEdits
in perf "evalCmd" $ ExceptT $
pluginWithIndefiniteProgress "Evaluating" mtoken Cancellable $ \_updater ->
runExceptT $ response' cmd
-- | Create an HscEnv which is suitable for performing interactive evaluation.
-- All necessary home modules will have linkables and the current module will
-- also be loaded into the environment.
--
-- The interactive context and interactive dynamic flags are also set appropiately.
initialiseSessionForEval :: Bool -> IdeState -> NormalizedFilePath -> IO HscEnv
initialiseSessionForEval needs_quickcheck st nfp = do
(ms, env1) <- runAction "runEvalCmd" st $ do
ms <- msrModSummary <$> use_ GetModSummary nfp
deps_hsc <- hscEnv <$> use_ GhcSessionDeps nfp
linkables_needed <- transitiveDeps <$> useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph nfp <*> pure nfp
linkables <- uses_ GetLinkable (nfp : maybe [] transitiveModuleDeps linkables_needed)
-- We unset the global rdr env in mi_globals when we generate interfaces
-- See Note [Clearing mi_globals after generating an iface]
-- However, the eval plugin (setContext specifically) requires the rdr_env
-- for the current module - so get it from the Typechecked Module and add
-- it back to the iface for the current module.
tm <- tmrTypechecked <$> use_ TypeCheck nfp
let rdr_env = tcg_rdr_env tm
let linkable_hsc = loadModulesHome (map (addRdrEnv . linkableHomeMod) linkables) deps_hsc
addRdrEnv hmi
| iface <- hm_iface hmi
, ms_mod ms == mi_module iface
#if MIN_VERSION_ghc(9,11,0)
= hmi { hm_iface = set_mi_top_env (Just $ IfaceTopEnv (forceGlobalRdrEnv (globalRdrEnvLocal rdr_env)) (mkIfaceImports $ tcg_import_decls tm)) iface}
#else
= hmi { hm_iface = iface { mi_globals = Just $!
#if MIN_VERSION_ghc(9,8,0)
forceGlobalRdrEnv
#endif
rdr_env
}}
#endif
| otherwise = hmi
return (ms, linkable_hsc)
-- Bit awkward we need to use evalGhcEnv here but setContext requires to run
-- in the Ghc monad
env2 <- liftIO $ evalGhcEnv env1 $ do
setContext [Compat.IIModule (moduleName (ms_mod ms))]
let df = flip xopt_set LangExt.ExtendedDefaultRules
. flip xopt_unset LangExt.MonomorphismRestriction
. flip gopt_set Opt_ImplicitImportQualified
. flip gopt_unset Opt_DiagnosticsShowCaret
. setBackend ghciBackend
$ (ms_hspp_opts ms) {
useColor = Never
, canUseColor = False }
modifyDynFlags (const df)
when needs_quickcheck $ void $ addPackages ["QuickCheck"]
getSession
return env2
#if MIN_VERSION_ghc(9,11,0)
mkIfaceImports :: [ImportUserSpec] -> [IfaceImport]
mkIfaceImports = map go
where
go (ImpUserSpec decl ImpUserAll) = IfaceImport decl ImpIfaceAll
go (ImpUserSpec decl (ImpUserExplicit env)) = IfaceImport decl (ImpIfaceExplicit (forceGlobalRdrEnv env))
go (ImpUserSpec decl (ImpUserEverythingBut ns)) = IfaceImport decl (ImpIfaceEverythingBut ns)
#endif
addFinalReturn :: Text -> [TextEdit] -> [TextEdit]
addFinalReturn mdlText edits
| not (null edits) && not (T.null mdlText) && T.last mdlText /= '\n' =
finalReturn mdlText : edits
| otherwise = edits
finalReturn :: Text -> TextEdit
finalReturn txt =
let ls = T.lines txt
l = fromIntegral $ length ls -1
c = fromIntegral $ T.length $ maybe T.empty snd (unsnoc ls)
p = Position l c
in TextEdit (Range p p) "\n"
moduleText :: IdeState -> Uri -> ExceptT PluginError (HandlerM config) Text
moduleText state uri = do
contents <-
handleMaybeM (PluginInternalError "mdlText") $
liftIO $
runAction "eval.getUriContents" state $
getUriContents $
toNormalizedUri uri
pure $ Rope.toText contents
testsBySection :: [Section] -> [(Section, EvalId, Test)]
testsBySection sections =
[(section, ident, test)
| (ident, section) <- zip [0..] sections
, test <- sectionTests section
]
type TEnv = String
-- |GHC declarations required for expression evaluation
evalSetup :: Ghc ()
evalSetup = do
preludeAsP <- parseImportDecl "import qualified Prelude as P"
context <- getContext
setContext (IIDecl preludeAsP : context)
runTests :: Recorder (WithPriority Log) -> EvalConfig -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
runTests recorder EvalConfig{..} e tests = do
df <- getInteractiveDynFlags
evalSetup
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals recorder True e df propSetup
mapM (processTest e df) tests
where
processTest :: TEnv -> DynFlags -> (Section, Test) -> Ghc TextEdit
processTest fp df (section, test) = do
let dbg = logWith recorder Debug
let pad = pad_ $ (if isLiterate fp then ("> " `T.append`) else id) $ padPrefix (sectionFormat section)
rs <- runTest e df test
dbg $ LogRunTestResults rs
let checkedResult = testCheck eval_cfg_diff (section, test) rs
let resultLines = concatMap T.lines checkedResult
let edit = asEdit (sectionFormat section) test (map pad resultLines)
dbg $ LogRunTestEdits edit
return edit
-- runTest :: String -> DynFlags -> Loc Test -> Ghc [Text]
runTest _ df test
| not (hasQuickCheck df) && isProperty test =
return $
singleLine
"Add QuickCheck to your cabal dependencies to run this test."
runTest e df test = evals recorder (eval_cfg_exception && not (isProperty test)) e df (asStatements test)
asEdit :: Format -> Test -> [Text] -> TextEdit
asEdit (MultiLine commRange) test resultLines
-- A test in a block comment, ending with @-\}@ without newline in-between.
| testRange test ^. L.end . L.line == commRange ^. L.end . L.line
=
TextEdit
(Range
(testRange test ^. L.end)
(resultRange test ^. L.end)
)
("\n" <> T.unlines (resultLines <> ["-}"]))
asEdit _ test resultLines =
TextEdit (resultRange test) (T.unlines resultLines)
{- |
The result of evaluating a test line can be:
* a value
* nothing
* a (possibly multiline) error message
A value is returned for a correct expression.
Either a pure value:
>>> 'h' : "askell"
"haskell"
Or an 'IO a' (output on stdout/stderr is captured):
>>> putStrLn "Hello," >> pure "World!"
Hello,
"World!"
Note the quotes around @World!@, which are a result of using 'show'.
Nothing is returned for a correct directive:
>>>:set -XFlexibleInstances
>>> import Data.Maybe
Nothing is returned for a correct declaration (let..,x=, data, class)
>>> let x = 11
>>> y = 22
>>> data B = T | F
>>> class C a
Nothing is returned for an empty line:
>>>
A, possibly multi line, error is returned for a wrong declaration, directive or value or an exception thrown by the evaluated code:
>>>:set -XNonExistent
Some flags have not been recognized: -XNonExistent
>>> cls C
Illegal term-level use of the class `C'
defined at <interactive>:1:2
In the first argument of `cls', namely `C'
In the expression: cls C
In an equation for `it_a1kSJ': it_a1kSJ = cls C
Variable not in scope: cls :: t0_a1kU9[tau:1] -> t1_a1kUb[tau:1]
>>> "A
lexical error at end of input
Exceptions are shown as if printed, but it can be configured to include prefix like
in GHCi or doctest. This allows it to be used as a hack to simulate print until we
get proper IO support. See #1977
>>> 3 `div` 0
divide by zero
>>> error "Something went wrong\nbad times" :: E.SomeException
Something went wrong
bad times
Or for a value that does not have a Show instance and can therefore not be displayed:
>>> data V = V
>>> V
No instance for `Show V' arising from a use of `evalPrint'
In a stmt of an interactive GHCi command: evalPrint it_a1l4V
-}
evals :: Recorder (WithPriority Log) -> Bool -> TEnv -> DynFlags -> [Statement] -> Ghc [Text]
evals recorder mark_exception fp df stmts = do
er <- gStrictTry $ mapM eval stmts
return $ case er of
Left err -> errorLines err
Right rs -> concat . catMaybes $ rs
where
dbg = logWith recorder Debug
eval :: Statement -> Ghc (Maybe [Text])
eval (Located l stmt)
| -- GHCi flags
Just (words -> flags) <- parseSetFlags stmt = do
dbg $ LogEvalFlags flags
ndf <- getInteractiveDynFlags
dbg $ LogEvalPreSetDynFlags ndf
eans <-
liftIO $ try @GhcException $
parseDynamicFlagsCmdLine ndf
(map (L $ UnhelpfulSpan unhelpfulReason) flags)
dbg $ LogEvalParsedFlags eans
case eans of
Left err -> pure $ Just $ errorLines $ show err
Right (df', ignoreds, warns) -> do
let warnings = do
guard $ not $ null warns
pure $ errorLines $
prettyWarnings warns
igns = do
guard $ not $ null ignoreds
pure
["Some flags have not been recognized: "
<> T.pack (intercalate ", " $ map SrcLoc.unLoc ignoreds)
]
dbg $ LogEvalPostSetDynFlags df'
setSessionAndInteractiveDynFlags df'
pure $ warnings <> igns
| -- A type/kind command
Just (cmd, arg) <- parseGhciLikeCmd $ T.pack stmt =
evalGhciLikeCmd cmd arg
| -- A statement
isStmt pf stmt =
do
dbg $ LogEvalStmtStart stmt
res <- exec stmt l
let r = case res of
Left err -> Just . (if mark_exception then exceptionLines else errorLines) $ err
Right x -> singleLine <$> x
dbg $ LogEvalStmtResult r
return r
| -- An import
isImport pf stmt =
do
dbg $ LogEvalImport stmt
_ <- addImport stmt
return Nothing
| -- A declaration
otherwise =
do
dbg $ LogEvalDeclaration stmt
void $ runDecls stmt
return Nothing
pf = initParserOpts df
unhelpfulReason = UnhelpfulInteractive
exec stmt l =
let opts = execOptions{execSourceFile = fp, execLineNumber = l}
in myExecStmt stmt opts
needsQuickCheck :: [(Section, Test)] -> Bool
needsQuickCheck = any (isProperty . snd)
hasQuickCheck :: DynFlags -> Bool
hasQuickCheck df = hasPackage df "QuickCheck"
singleLine :: String -> [Text]
singleLine s = [T.pack s]
{- |
Convert error messages to a list of text lines
Remove unnecessary information.
-}
errorLines :: String -> [Text]
errorLines =
dropWhileEnd T.null
. takeWhile (not . (\x -> "CallStack" `T.isPrefixOf` x || "HasCallStack" `T.isPrefixOf` x))
. T.lines
. T.pack
{- |
Convert exception messages to a list of text lines
Remove unnecessary information and mark it as exception.
We use '*** Exception:' to make it identical to doctest
output, see #2353.
-}
exceptionLines :: String -> [Text]
exceptionLines = (ix 0 %~ ("*** Exception: " <>)) . errorLines
{- |
>>> map (pad_ (T.pack "--")) (map T.pack ["2+2",""])
["--2+2","--<BLANKLINE>"]
-}
pad_ :: Text -> Text -> Text
pad_ prefix = (prefix `T.append`) . convertBlank
convertBlank :: Text -> Text
convertBlank x
| T.null x = "<BLANKLINE>"
| otherwise = x
padPrefix :: IsString p => Format -> p
padPrefix SingleLine = "-- "
padPrefix _ = ""
{- | Resulting @Text@ MUST NOT prefix each line with @--@
Such comment-related post-process will be taken place
solely in 'evalGhciLikeCmd'.
-}
type GHCiLikeCmd = DynFlags -> Text -> Ghc (Maybe Text)
-- Should we use some sort of trie here?
ghciLikeCommands :: [(Text, GHCiLikeCmd)]
ghciLikeCommands =
[ ("info", doInfoCmd False)
, ("info!", doInfoCmd True)
, ("kind", doKindCmd False)
, ("kind!", doKindCmd True)
, ("type", doTypeCmd)
]
evalGhciLikeCmd :: Text -> Text -> Ghc (Maybe [Text])
evalGhciLikeCmd cmd arg = do
df <- getSessionDynFlags
case lookup cmd ghciLikeCommands
<|> snd
<$> find (T.isPrefixOf cmd . fst) ghciLikeCommands of
Just hndler ->
fmap
T.lines
<$> hndler df arg
_ -> E.throw $ GhciLikeCmdNotImplemented cmd arg
doInfoCmd :: Bool -> DynFlags -> Text -> Ghc (Maybe Text)
doInfoCmd allInfo dflags s = do
sdocs <- mapM infoThing (T.words s)
pure $ Just $ T.pack $ showSDoc dflags (vcat sdocs)
where
infoThing :: GHC.GhcMonad m => Text -> m SDoc
infoThing (T.unpack -> str) = do
names <- GHC.parseName str
mb_stuffs <- mapM (GHC.getInfo allInfo) names
let filtered = filterOutChildren (\(t,_f,_ci,_fi,_sd) -> t)
(catMaybes $ toList mb_stuffs)
return $ vcat (intersperse (text "") $ map pprInfo filtered)
filterOutChildren :: (a -> TyThing) -> [a] -> [a]
filterOutChildren get_thing xs
= filter (not . has_parent) xs
where
all_names = mkNameSet (map (getName . get_thing) xs)
has_parent x = case tyThingParent_maybe (get_thing x) of
Just p -> getName p `elemNameSet` all_names
Nothing -> False
pprInfo :: (TyThing, Fixity, [GHC.ClsInst], [GHC.FamInst], SDoc) -> SDoc
pprInfo (thing, fixity, cls_insts, fam_insts, docs)
= docs
$$ pprTyThingInContextLoc thing
$$ showFixity thing fixity
$$ vcat (map GHC.pprInstance cls_insts)
$$ vcat (map GHC.pprFamInst fam_insts)
pprTyThingInContextLoc :: TyThing -> SDoc
pprTyThingInContextLoc tyThing
= showWithLoc (pprDefinedAt (getName tyThing))
(pprTyThingInContext showToHeader tyThing)
showWithLoc :: SDoc -> SDoc -> SDoc
showWithLoc loc doc
= hang doc 2 (text "\t--" <+> loc)
showFixity :: TyThing -> Fixity -> SDoc
showFixity thing fixity
| fixity /= GHC.defaultFixity || isSymOcc (getOccName thing)
= ppr fixity <+> pprInfixName (GHC.getName thing)
| otherwise = empty
doKindCmd :: Bool -> DynFlags -> Text -> Ghc (Maybe Text)
doKindCmd False df arg = do
let input = T.strip arg
(_, kind) <- typeKind False $ T.unpack input
let kindText = text (T.unpack input) <+> "::" <+> pprSigmaType kind
pure $ Just $ T.pack (showSDoc df kindText)
doKindCmd True df arg = do
let input = T.strip arg
(ty, kind) <- typeKind True $ T.unpack input
let kindDoc = text (T.unpack input) <+> "::" <+> pprSigmaType kind
tyDoc = "=" <+> pprSigmaType ty
pure $ Just $ T.pack (showSDoc df $ kindDoc $$ tyDoc)
doTypeCmd :: DynFlags -> Text -> Ghc (Maybe Text)
doTypeCmd dflags arg = do
let (emod, expr) = parseExprMode arg
ty <- GHC.exprType emod $ T.unpack expr
let rawType = T.strip $ T.pack $ showSDoc dflags $ pprSigmaType ty
broken = T.any (\c -> c == '\r' || c == '\n') rawType
pure $
Just $
if broken
then
T.pack $
showSDoc dflags $
text (T.unpack expr)
$$ nest 2 ("::" <+> pprSigmaType ty)
else expr <> " :: " <> rawType <> "\n"
parseExprMode :: Text -> (TcRnExprMode, T.Text)
parseExprMode rawArg = case T.break isSpace rawArg of
("+d", rest) -> (TM_Default, T.strip rest)
_ -> (TM_Inst, rawArg)
data GhciLikeCmdException = GhciLikeCmdNotImplemented
{ ghciCmdName :: Text
, ghciCmdArg :: Text
}
instance Show GhciLikeCmdException where
showsPrec _ GhciLikeCmdNotImplemented{..} =
showString "unknown command '"
. showString (T.unpack ghciCmdName)
. showChar '\''
instance E.Exception GhciLikeCmdException
{-
>>> parseGhciLikeCmd (T.pack ":kind! N + M + 1")
Just ("kind!","N + M + 1")
>>> parseGhciLikeCmd (T.pack ":kind a")
Just ("kind","a")
-}
parseGhciLikeCmd :: Text -> Maybe (Text, Text)
parseGhciLikeCmd input = do
(':', rest) <- T.uncons $ T.stripStart input
pure $ second T.strip $ T.break isSpace rest