This PR fixes a bug where the unknown identifier code actions wouldn't work correctly for some unknown identifier error spans and adjusts several unknown identifier spans to actually end on the identifier in question. The following additional adjustments are made: - The fallback mechanism of the unknown identifier code actions is removed, since it could produce severely incorrect suggestions for unknown identifier errors on fields. - A performance bug when using the code action to import all unknown identifiers is fixed. - A bug that occurs when the elaborator produces multiple overlapping completion infos is fixed. - A bug in the snapshot selection that could cause it to wait for snapshots in snapshots with non-canonical syntax is fixed. - Some invariants of the snapshot tree are documented. - The snapshot tree formatting is adjusted to display the final info tree again.
641 lines
27 KiB
Text
641 lines
27 KiB
Text
/-
|
||
Copyright (c) 2021 Wojciech Nawrocki. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Wojciech Nawrocki, Marc Huisinga
|
||
-/
|
||
prelude
|
||
import Lean.Server.FileWorker.ExampleHover
|
||
import Lean.Server.FileWorker.InlayHints
|
||
import Lean.Server.FileWorker.SemanticHighlighting
|
||
import Lean.Server.Completion
|
||
import Lean.Server.References
|
||
|
||
import Lean.Widget.Diff
|
||
|
||
namespace Lean.Server.FileWorker
|
||
open Lsp
|
||
open RequestM
|
||
open Snapshots
|
||
|
||
open Lean.Parser.Tactic.Doc (alternativeOfTactic getTacticExtensionString)
|
||
|
||
def findCompletionCmdDataAtPos
|
||
(doc : EditableDocument)
|
||
(pos : String.Pos)
|
||
: ServerTask (Option (Syntax × Elab.InfoTree)) :=
|
||
-- `findCmdDataAtPos` may produce an incorrect snapshot when `pos` is in whitespace.
|
||
-- However, most completions don't need trailing whitespace at the term level;
|
||
-- synthetic completions are the only notions of completion that care care about whitespace.
|
||
-- Synthetic tactic completion only needs the `ContextInfo` of the command, so any snapshot
|
||
-- will do.
|
||
-- Synthetic field completion in `{ }` doesn't care about whitespace;
|
||
-- synthetic field completion in `where` only needs to gather the expected type.
|
||
findCmdDataAtPos doc pos (includeStop := true)
|
||
|
||
def handleCompletion (p : CompletionParams)
|
||
: RequestM (RequestTask CompletionList) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let pos := text.lspPosToUtf8Pos p.position
|
||
let caps := (← read).initParams.capabilities
|
||
mapTaskCostly (findCompletionCmdDataAtPos doc pos) fun cmdData? => do
|
||
let some (cmdStx, infoTree) := cmdData?
|
||
| return { items := #[], isIncomplete := true }
|
||
Completion.find? p doc.meta.text pos cmdStx infoTree caps
|
||
|
||
/--
|
||
Handles `completionItem/resolve` requests that are sent by the client after the user selects
|
||
a completion item that was provided by `textDocument/completion`. Resolving the item fills the
|
||
`detail?` field of the item with the pretty-printed type.
|
||
This control flow is necessary because pretty-printing the type for every single completion item
|
||
(even those never selected by the user) is inefficient.
|
||
-/
|
||
def handleCompletionItemResolve (item : CompletionItem)
|
||
: RequestM (RequestTask CompletionItem) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let some (data : ResolvableCompletionItemData) := item.data?.bind fun data => (fromJson? data).toOption
|
||
| return .pure item
|
||
let some id := data.id?
|
||
| return .pure item
|
||
let pos := text.lspPosToUtf8Pos data.params.position
|
||
mapTaskCostly (findCompletionCmdDataAtPos doc pos) fun cmdData? => do
|
||
let some (cmdStx, infoTree) := cmdData?
|
||
| return item
|
||
Completion.resolveCompletionItem? text pos cmdStx infoTree item id data.cPos
|
||
|
||
open Elab in
|
||
def handleHover (p : HoverParams)
|
||
: RequestM (RequestTask (Option Hover)) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let mkHover (s : String) (r : String.Range) : Hover :=
|
||
let s := Hover.rewriteExamples s
|
||
{
|
||
contents := {
|
||
kind := MarkupKind.markdown
|
||
value := s
|
||
}
|
||
range? := r.toLspRange text
|
||
}
|
||
|
||
let hoverPos := text.lspPosToUtf8Pos p.position
|
||
withWaitFindSnap doc (fun s => s.endPos > hoverPos)
|
||
(notFoundX := pure none) fun snap => do
|
||
-- try to find parser docstring from syntax tree
|
||
let stack? := snap.stx.findStack? (·.getRange?.any (·.contains hoverPos))
|
||
let stxDoc? ← match stack? with
|
||
| some stack => stack.findSomeM? fun (stx, _) => do
|
||
let .node _ kind _ := stx | pure none
|
||
let docStr ← findDocString? snap.env kind
|
||
return docStr.map (·, stx.getRange?.get!)
|
||
| none => pure none
|
||
|
||
-- now try info tree
|
||
if let some ictx := snap.infoTree.hoverableInfoAt? hoverPos then
|
||
if let some range := ictx.info.range? then
|
||
-- prefer info tree if at least as specific as parser docstring
|
||
if stxDoc?.all fun (_, stxRange) => stxRange.includes range then
|
||
if let some hoverFmt ← ictx.info.fmtHover? ictx.ctx then
|
||
return mkHover (toString hoverFmt.fmt) range
|
||
|
||
if let some (doc, range) := stxDoc? then
|
||
return mkHover doc range
|
||
|
||
return none
|
||
|
||
open Elab GoToKind in
|
||
def locationLinksOfInfo (kind : GoToKind) (ictx : InfoWithCtx)
|
||
(infoTree? : Option InfoTree := none) : RequestM (Array LocationLink) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
|
||
let locationLinksFromDecl (i : Elab.Info) (n : Name) :=
|
||
locationLinksFromDecl doc.meta.uri n <| (·.toLspRange text) <$> i.range?
|
||
|
||
let locationLinksFromBinder (i : Elab.Info) (id : FVarId) := do
|
||
if let some i' := infoTree? >>= InfoTree.findInfo? fun
|
||
| Info.ofTermInfo { isBinder := true, expr := Expr.fvar id' .., .. } => id' == id
|
||
| _ => false then
|
||
if let some r := i'.range? then
|
||
let r := r.toLspRange text
|
||
let ll : LocationLink := {
|
||
originSelectionRange? := (·.toLspRange text) <$> i.range?
|
||
targetUri := doc.meta.uri
|
||
targetRange := r
|
||
targetSelectionRange := r
|
||
}
|
||
return #[ll]
|
||
return #[]
|
||
|
||
let locationLinksFromImport (i : Elab.Info) := do
|
||
let `(Parser.Module.import| $[private]? import $[all]? $mod) := i.stx
|
||
| return #[]
|
||
if let some modUri ← documentUriFromModule? mod.getId then
|
||
let range := { start := ⟨0, 0⟩, «end» := ⟨0, 0⟩ : Range }
|
||
let ll : LocationLink := {
|
||
originSelectionRange? := (·.toLspRange text) <$> mod.raw.getRange? (canonicalOnly := true)
|
||
targetUri := modUri
|
||
targetRange := range
|
||
targetSelectionRange := range
|
||
}
|
||
return #[ll]
|
||
return #[]
|
||
|
||
let i := ictx.info
|
||
let ci := ictx.ctx
|
||
let children := ictx.children
|
||
|
||
let locationLinksDefault : RequestM (Array LocationLink) := do
|
||
-- If other go-tos fail, we try to show the elaborator or parser
|
||
if let some ei := i.toElabInfo? then
|
||
if kind == declaration && ci.env.contains ei.stx.getKind then
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromDecl i ei.stx.getKind
|
||
if kind == definition && ci.env.contains ei.elaborator then
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromDecl i ei.elaborator
|
||
return #[]
|
||
|
||
let locationLinksFromTermInfo (ti : TermInfo) : RequestM (Array LocationLink) := do
|
||
let mut expr := ti.expr
|
||
if kind == type then
|
||
expr ← ci.runMetaM i.lctx do
|
||
return Expr.getAppFn (← instantiateMVars (← Meta.inferType expr))
|
||
match expr.consumeMData with
|
||
| Expr.const n .. => return ← ci.runMetaM i.lctx <| locationLinksFromDecl i n
|
||
| Expr.fvar id .. => return ← ci.runMetaM i.lctx <| locationLinksFromBinder i id
|
||
| _ => pure ()
|
||
|
||
-- Check whether this `TermInfo` node is directly responsible for its `.expr`.
|
||
-- This is the case iff all of its children represent strictly smaller subexpressions;
|
||
-- it is sufficient to check this of all direct children of this node (and that its elaborator didn't expand it as a macro)
|
||
let isExprGenerator := children.all fun
|
||
| .node (Info.ofTermInfo info) _ => info.expr != expr
|
||
| .node (Info.ofMacroExpansionInfo _) _ => false
|
||
| _ => true
|
||
|
||
-- don't go-to-instance if this `TermInfo` didn't directly generate its `.expr`
|
||
if kind != declaration && isExprGenerator then
|
||
-- go-to-definition on a projection application of a typeclass
|
||
-- should return all instances generated by TC
|
||
expr ← ci.runMetaM i.lctx do instantiateMVars expr
|
||
if let .const n _ := expr.getAppFn.consumeMData then
|
||
-- also include constant along with instance results
|
||
let mut results ← ci.runMetaM i.lctx <| locationLinksFromDecl i n
|
||
if let some info := ci.env.getProjectionFnInfo? n then
|
||
let mut elaborators := default
|
||
if let some ei := i.toElabInfo? then do
|
||
-- also include elaborator along with instance results, as this wouldn't be accessible otherwise
|
||
if ei.elaborator != `Delab -- prevent an error if this `TermInfo` came from the infoview
|
||
&& ei.elaborator != `Lean.Elab.Term.elabApp && ei.elaborator != `Lean.Elab.Term.elabIdent -- don't include trivial elaborators
|
||
then do
|
||
elaborators ← ci.runMetaM i.lctx <| locationLinksFromDecl i ei.elaborator
|
||
let instIdx := info.numParams
|
||
let appArgs := expr.getAppArgs
|
||
let rec extractInstances : Expr → RequestM (Array Name)
|
||
| .const declName _ => do
|
||
if ← ci.runMetaM i.lctx do Lean.Meta.isInstance declName then pure #[declName] else pure #[]
|
||
| .app fn arg => do pure $ (← extractInstances fn).append (← extractInstances arg)
|
||
| .mdata _ e => extractInstances e
|
||
| _ => pure #[]
|
||
if let some instArg := appArgs[instIdx]? then
|
||
for inst in (← extractInstances instArg) do
|
||
results := results.append (← ci.runMetaM i.lctx <| locationLinksFromDecl i inst)
|
||
results := results.append elaborators -- put elaborators at the end of the results
|
||
return results
|
||
locationLinksDefault
|
||
|
||
match i with
|
||
| .ofTermInfo ti =>
|
||
return ← locationLinksFromTermInfo ti
|
||
| .ofDelabTermInfo { toTermInfo := ti, location?, .. } =>
|
||
if let some location := location? then
|
||
if let some targetUri ← documentUriFromModule? location.module then
|
||
let range := location.range.toLspRange
|
||
let result : LocationLink := {
|
||
targetUri, targetRange := range, targetSelectionRange := range,
|
||
originSelectionRange? := (·.toLspRange text) <$> i.range?
|
||
}
|
||
return #[result]
|
||
-- If we fail to find a DocumentUri, fall through and use the default method to at least try to have something to jump to.
|
||
return ← locationLinksFromTermInfo ti
|
||
| .ofFieldInfo fi =>
|
||
if kind == type then
|
||
let expr ← ci.runMetaM i.lctx do
|
||
instantiateMVars (← Meta.inferType fi.val)
|
||
if let some n := expr.getAppFn.constName? then
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromDecl i n
|
||
else
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromDecl i fi.projName
|
||
| .ofOptionInfo oi =>
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromDecl i oi.declName
|
||
| .ofCommandInfo ⟨`import, _⟩ =>
|
||
if kind == definition || kind == declaration then
|
||
return ← ci.runMetaM i.lctx <| locationLinksFromImport i
|
||
| _ => pure ()
|
||
locationLinksDefault
|
||
|
||
open Elab GoToKind in
|
||
def handleDefinition (kind : GoToKind) (p : TextDocumentPositionParams)
|
||
: RequestM (RequestTask (Array LocationLink)) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let hoverPos := text.lspPosToUtf8Pos p.position
|
||
|
||
withWaitFindSnap doc (fun s => s.endPos >= hoverPos)
|
||
(notFoundX := pure #[]) fun snap => do
|
||
if let some infoWithCtx := snap.infoTree.hoverableInfoAt? (omitIdentApps := true) (includeStop := true /- #767 -/) hoverPos then
|
||
locationLinksOfInfo kind infoWithCtx snap.infoTree
|
||
else return #[]
|
||
|
||
open Language in
|
||
def findGoalsAt? (doc : EditableDocument) (hoverPos : String.Pos) : ServerTask (Option (List Elab.GoalsAtResult)) :=
|
||
let text := doc.meta.text
|
||
findCmdParsedSnap doc hoverPos |>.bindCostly fun
|
||
| some cmdParsed =>
|
||
let t := toSnapshotTree cmdParsed |>.foldSnaps [] fun snap oldGoals => Id.run do
|
||
let some stx := snap.stx?
|
||
| return .pure (oldGoals, .proceed (foldChildren := false))
|
||
let some (pos, tailPos, trailingPos) := getPositions stx
|
||
| return .pure (oldGoals, .proceed (foldChildren := true))
|
||
let snapRange : String.Range := ⟨pos, trailingPos⟩
|
||
-- When there is no trailing whitespace, we also consider snapshots directly before the
|
||
-- cursor.
|
||
let hasNoTrailingWhitespace := tailPos == trailingPos
|
||
if ! text.rangeContainsHoverPos snapRange hoverPos (includeStop := hasNoTrailingWhitespace) then
|
||
return .pure (oldGoals, .proceed (foldChildren := false))
|
||
|
||
return snap.task.asServerTask.mapCheap fun tree => Id.run do
|
||
let some infoTree := tree.element.infoTree?
|
||
| return (oldGoals, .proceed (foldChildren := true))
|
||
|
||
let goals := infoTree.goalsAt? text hoverPos
|
||
let optimalSnapRange : String.Range := ⟨pos, tailPos⟩
|
||
let isOptimalGoalSet :=
|
||
text.rangeContainsHoverPos optimalSnapRange hoverPos
|
||
(includeStop := hasNoTrailingWhitespace)
|
||
|| goals.any fun goal => ! goal.indented
|
||
if isOptimalGoalSet then
|
||
return (goals, .done)
|
||
|
||
return (goals, .proceed (foldChildren := true))
|
||
t.mapCheap fun
|
||
| [] => none
|
||
| goals => goals
|
||
| none =>
|
||
.pure none
|
||
where
|
||
getPositions (stx : Syntax) : Option (String.Pos × String.Pos × String.Pos) := do
|
||
let pos ← stx.getPos? (canonicalOnly := true)
|
||
let tailPos ← stx.getTailPos? (canonicalOnly := true)
|
||
let trailingPos? ← stx.getTrailingTailPos? (canonicalOnly := true)
|
||
return (pos, tailPos, trailingPos?)
|
||
|
||
open RequestM in
|
||
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option Widget.InteractiveGoals)) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let hoverPos := text.lspPosToUtf8Pos p.position
|
||
mapTaskCostly (findGoalsAt? doc hoverPos) <| Option.mapM fun rs => do
|
||
let goals : List Widget.InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
|
||
let ciAfter := { ci with mctx := ti.mctxAfter }
|
||
let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore }
|
||
-- compute the interactive goals
|
||
let goals ← ci.runMetaM {} (do
|
||
let goals := List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
|
||
let goals ← goals.mapM Widget.goalToInteractive
|
||
return ⟨goals⟩
|
||
)
|
||
-- compute the goal diff
|
||
ciAfter.runMetaM {} (do
|
||
try
|
||
Widget.diffInteractiveGoals useAfter ti goals
|
||
catch _ =>
|
||
-- fail silently, since this is just a bonus feature
|
||
return goals
|
||
)
|
||
return goals.foldl (· ++ ·) ∅
|
||
|
||
open Elab in
|
||
def handlePlainGoal (p : PlainGoalParams)
|
||
: RequestM (RequestTask (Option PlainGoal)) := do
|
||
let t ← getInteractiveGoals p
|
||
return t.mapCheap <| Except.map <| Option.map <| fun {goals, ..} =>
|
||
if goals.isEmpty then
|
||
{ goals := #[], rendered := "no goals" }
|
||
else
|
||
let goalStrs := goals.map (toString ·.pretty)
|
||
let goalBlocks := goalStrs.map fun goal => s!"```lean
|
||
{goal}
|
||
```"
|
||
let md := String.intercalate "\n---\n" goalBlocks.toList
|
||
{ goals := goalStrs, rendered := md }
|
||
|
||
def getInteractiveTermGoal (p : Lsp.PlainTermGoalParams)
|
||
: RequestM (RequestTask (Option Widget.InteractiveTermGoal)) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let hoverPos := text.lspPosToUtf8Pos p.position
|
||
mapTaskCostly (findInfoTreeAtPos doc hoverPos (includeStop := true)) <| Option.bindM fun infoTree => do
|
||
let some {ctx := ci, info := i@(Elab.Info.ofTermInfo ti), ..} := infoTree.termGoalAt? hoverPos
|
||
| return none
|
||
let ty ← ci.runMetaM i.lctx do
|
||
instantiateMVars <| ti.expectedType?.getD (← Meta.inferType ti.expr)
|
||
-- for binders, hide the last hypothesis (the binder itself)
|
||
let lctx' := if ti.isBinder then i.lctx.pop else i.lctx
|
||
let goal ← ci.runMetaM lctx' do
|
||
Widget.goalToInteractive (← Meta.mkFreshExprMVar ty).mvarId!
|
||
let range := if let some r := i.range? then r.toLspRange text else ⟨p.position, p.position⟩
|
||
return some { goal with range, term := ⟨ti⟩ }
|
||
|
||
def handlePlainTermGoal (p : PlainTermGoalParams)
|
||
: RequestM (RequestTask (Option PlainTermGoal)) := do
|
||
let t ← getInteractiveTermGoal p
|
||
return t.mapCheap <| Except.map <| Option.map fun goal =>
|
||
{ goal := toString goal.pretty
|
||
range := goal.range
|
||
}
|
||
|
||
partial def handleDocumentHighlight (p : DocumentHighlightParams)
|
||
: RequestM (RequestTask (Array DocumentHighlight)) := do
|
||
let doc ← readDoc
|
||
let text := doc.meta.text
|
||
let pos := text.lspPosToUtf8Pos p.position
|
||
|
||
let rec highlightReturn? (doRange? : Option Range) : Syntax → Option DocumentHighlight
|
||
| `(doElem|return%$i $e) => Id.run do
|
||
if let some range := i.getRange? then
|
||
if range.contains pos then
|
||
return some { range := doRange?.getD (range.toLspRange text), kind? := DocumentHighlightKind.text }
|
||
highlightReturn? doRange? e
|
||
| `(do%$i $elems) => highlightReturn? (i.getRange?.get!.toLspRange text) elems
|
||
| stx => stx.getArgs.findSome? (highlightReturn? doRange?)
|
||
|
||
let highlightRefs? (snaps : Array Snapshot) : IO (Option (Array DocumentHighlight)) := do
|
||
let trees := snaps.map (·.infoTree)
|
||
let refs : Lsp.ModuleRefs ← findModuleRefs text trees |>.toLspModuleRefs
|
||
let mut ranges := #[]
|
||
for ident in refs.findAt p.position (includeStop := true) do
|
||
if let some info := refs.get? ident then
|
||
if let some ⟨definitionRange, _⟩ := info.definition? then
|
||
ranges := ranges.push definitionRange
|
||
ranges := ranges.append <| info.usages.map (·.range)
|
||
if ranges.isEmpty then
|
||
return none
|
||
return some <| ranges.map ({ range := ·, kind? := DocumentHighlightKind.text })
|
||
|
||
withWaitFindSnap doc (fun s => s.endPos >= pos)
|
||
(notFoundX := pure #[]) fun snap => do
|
||
let (snaps, _) ← doc.cmdSnaps.getFinishedPrefix
|
||
if let some his ← highlightRefs? snaps.toArray then
|
||
return his
|
||
if let some hi := highlightReturn? none snap.stx then
|
||
return #[hi]
|
||
return #[]
|
||
|
||
structure NamespaceEntry where
|
||
/-- The list of the name components introduced by this namespace command,
|
||
in reverse order so that `end` will peel them off from the front. -/
|
||
name : List Name
|
||
stx : Syntax
|
||
selection : Syntax
|
||
prevSiblings : Array DocumentSymbol
|
||
|
||
def NamespaceEntry.finish (text : FileMap) (syms : Array DocumentSymbol) (endStx : Option Syntax) :
|
||
NamespaceEntry → Array DocumentSymbol
|
||
| { name, stx, selection, prevSiblings, .. } =>
|
||
-- we can assume that commands always have at least one position (see `parseCommand`)
|
||
let range := match endStx with
|
||
| some endStx => (mkNullNode #[stx, endStx]).getRange?.get!
|
||
| none => { stx.getRange?.get! with stop := text.source.endPos }
|
||
let name := name.foldr (fun x y => y ++ x) Name.anonymous
|
||
prevSiblings.push <| DocumentSymbol.mk {
|
||
-- anonymous sections are represented by `«»` name components
|
||
name := if name == `«» then "<section>" else name.toString
|
||
kind := .namespace
|
||
range := range.toLspRange text
|
||
selectionRange := selection.getRange?.getD range |>.toLspRange text
|
||
children? := syms
|
||
}
|
||
|
||
open Parser.Command in
|
||
partial def handleDocumentSymbol (_ : DocumentSymbolParams)
|
||
: RequestM (RequestTask DocumentSymbolResult) := do
|
||
let doc ← readDoc
|
||
-- bad: we have to wait on elaboration of the entire file before we can report document symbols
|
||
let t := doc.cmdSnaps.waitAll
|
||
mapTaskCostly t fun (snaps, _) => do
|
||
let mut stxs := snaps.map (·.stx)
|
||
return { syms := ← toDocumentSymbols doc.meta.text stxs #[] [] }
|
||
where
|
||
toDocumentSymbols (text : FileMap) (stxs : List Syntax)
|
||
(syms : Array DocumentSymbol) (stack : List NamespaceEntry) :
|
||
RequestM (Array DocumentSymbol) := do
|
||
RequestM.checkCancelled
|
||
match stxs with
|
||
| [] => return stack.foldl (fun syms entry => entry.finish text syms none) syms
|
||
| stx::stxs => match stx with
|
||
| `(namespace $id) =>
|
||
let entry := { name := id.getId.componentsRev, stx, selection := id, prevSiblings := syms }
|
||
toDocumentSymbols text stxs #[] (entry :: stack)
|
||
| `($_:sectionHeader section $(id)?) =>
|
||
let name := id.map (·.getId.componentsRev) |>.getD [`«»]
|
||
let entry := { name, stx, selection := id.map (·.raw) |>.getD stx, prevSiblings := syms }
|
||
toDocumentSymbols text stxs #[] (entry :: stack)
|
||
| `(end $(id)?) =>
|
||
let rec popStack n syms
|
||
| [] => toDocumentSymbols text stxs syms []
|
||
| entry :: stack =>
|
||
if entry.name.length == n then
|
||
let syms := entry.finish text syms stx
|
||
toDocumentSymbols text stxs syms stack
|
||
else if entry.name.length > n then
|
||
let syms := { entry with name := entry.name.take n, prevSiblings := #[] }.finish text syms stx
|
||
toDocumentSymbols text stxs syms ({ entry with name := entry.name.drop n } :: stack)
|
||
else
|
||
let syms := entry.finish text syms stx
|
||
popStack (n - entry.name.length) syms stack
|
||
popStack (id.map (·.getId.getNumParts) |>.getD 1) syms stack
|
||
| _ => do
|
||
unless stx.isOfKind ``Lean.Parser.Command.declaration do
|
||
return ← toDocumentSymbols text stxs syms stack
|
||
if let some stxRange := stx.getRange? then
|
||
let (name, selection) := match stx with
|
||
| `($_:declModifiers $_:attrKind instance $[$np:namedPrio]? $[$id$[.{$ls,*}]?]? $sig:declSig $_) =>
|
||
((·.getId.toString) <$> id |>.getD s!"instance {sig.raw.reprint.getD ""}", id.map (·.raw) |>.getD sig)
|
||
| _ =>
|
||
match stx.getArg 1 |>.getArg 1 with
|
||
| `(declId|$id$[.{$ls,*}]?) => (id.raw.getId.toString, id)
|
||
| _ =>
|
||
let stx10 : Syntax := (stx.getArg 1).getArg 0 -- TODO: stx[1][0] times out
|
||
(stx10.isIdOrAtom?.getD "<unknown>", stx10)
|
||
if let some selRange := selection.getRange? then
|
||
let sym := DocumentSymbol.mk {
|
||
name := name
|
||
kind := SymbolKind.method
|
||
range := stxRange.toLspRange text
|
||
selectionRange := selRange.toLspRange text
|
||
}
|
||
return ← toDocumentSymbols text stxs (syms.push sym) stack
|
||
toDocumentSymbols text stxs syms stack
|
||
|
||
partial def handleFoldingRange (_ : FoldingRangeParams)
|
||
: RequestM (RequestTask (Array FoldingRange)) := do
|
||
let doc ← readDoc
|
||
let t := doc.cmdSnaps.waitAll
|
||
mapTaskCostly t fun (snaps, _) => do
|
||
let stxs := snaps.map (·.stx)
|
||
let (_, ranges) ← StateT.run (addRanges doc.meta.text [] stxs) #[]
|
||
return ranges
|
||
where
|
||
isImport stx := stx.isOfKind ``Lean.Parser.Module.header || stx.isOfKind ``Lean.Parser.Command.open
|
||
|
||
addRanges (text : FileMap) sections
|
||
| [] => do
|
||
if let (_, start)::rest := sections then
|
||
addRange text FoldingRangeKind.region start text.source.endPos
|
||
addRanges text rest []
|
||
| stx::stxs => do
|
||
RequestM.checkCancelled
|
||
match stx with
|
||
| `(namespace $id) =>
|
||
addRanges text ((id.getId.getNumParts, stx.getPos?)::sections) stxs
|
||
| `($_:sectionHeader section $(id)?) =>
|
||
addRanges text ((id.map (·.getId.getNumParts) |>.getD 1, stx.getPos?)::sections) stxs
|
||
| `(end $(id)?) => do
|
||
let rec popRanges n sections := do
|
||
if let (size, start)::rest := sections then
|
||
if size == n then
|
||
addRange text FoldingRangeKind.region start stx.getTailPos?
|
||
addRanges text rest stxs
|
||
else if size > n then
|
||
-- we don't add a range here because vscode doesn't like
|
||
-- multiple folding regions with the same start line
|
||
addRanges text ((size - n, start)::rest) stxs
|
||
else
|
||
addRange text FoldingRangeKind.region start stx.getTailPos?
|
||
popRanges (n - size) rest
|
||
else
|
||
addRanges text sections stxs
|
||
popRanges (id.map (·.getId.getNumParts) |>.getD 1) sections
|
||
| `(mutual $body* end) => do
|
||
addRangeFromSyntax text FoldingRangeKind.region stx
|
||
addRanges text [] body.raw.toList
|
||
addRanges text sections stxs
|
||
| _ => do
|
||
if isImport stx then
|
||
let (imports, stxs) := stxs.span isImport
|
||
let last := imports.getLastD stx
|
||
addRange text FoldingRangeKind.imports stx.getPos? last.getTailPos?
|
||
addRanges text sections stxs
|
||
else
|
||
addCommandRange text stx
|
||
addRanges text sections stxs
|
||
|
||
addCommandRange text stx :=
|
||
match stx.getKind with
|
||
| `Lean.Parser.Command.moduleDoc =>
|
||
addRangeFromSyntax text FoldingRangeKind.comment stx
|
||
| ``Lean.Parser.Command.declaration => do
|
||
-- When visiting a declaration, attempt to fold the doc comment
|
||
-- separately to the main definition.
|
||
-- We never fold other modifiers, such as annotations.
|
||
if let `($dm:declModifiers $decl) := stx then
|
||
if let some comment := dm.raw[0].getOptional? then
|
||
addRangeFromSyntax text FoldingRangeKind.comment comment
|
||
|
||
addRangeFromSyntax text FoldingRangeKind.region decl
|
||
else
|
||
addRangeFromSyntax text FoldingRangeKind.region stx
|
||
| _ =>
|
||
addRangeFromSyntax text FoldingRangeKind.region stx
|
||
|
||
addRangeFromSyntax (text : FileMap) kind stx := addRange text kind stx.getPos? stx.getTailPos?
|
||
|
||
addRange (text : FileMap) kind start? stop? := do
|
||
if let (some startP, some endP) := (start?, stop?) then
|
||
let startP := text.utf8PosToLspPos startP
|
||
let endP := text.utf8PosToLspPos endP
|
||
if startP.line != endP.line then
|
||
modify fun st => st.push
|
||
{ startLine := startP.line
|
||
endLine := endP.line
|
||
kind? := some kind }
|
||
|
||
partial def handleWaitForDiagnostics (p : WaitForDiagnosticsParams)
|
||
: RequestM (RequestTask WaitForDiagnostics) := do
|
||
let rec waitLoop : RequestM EditableDocument := do
|
||
let doc ← readDoc
|
||
if p.version ≤ doc.meta.version then
|
||
return doc
|
||
else
|
||
IO.sleep 50
|
||
waitLoop
|
||
let t ← RequestM.asTask waitLoop
|
||
RequestM.bindTaskCheap t fun doc? => do
|
||
let doc ← liftExcept doc?
|
||
return doc.reporter.mapCheap (fun _ => pure WaitForDiagnostics.mk)
|
||
|
||
builtin_initialize
|
||
registerLspRequestHandler
|
||
"textDocument/waitForDiagnostics"
|
||
WaitForDiagnosticsParams
|
||
WaitForDiagnostics
|
||
handleWaitForDiagnostics
|
||
registerLspRequestHandler
|
||
"textDocument/completion"
|
||
CompletionParams
|
||
CompletionList
|
||
handleCompletion
|
||
registerLspRequestHandler
|
||
"completionItem/resolve"
|
||
CompletionItem
|
||
CompletionItem
|
||
handleCompletionItemResolve
|
||
registerLspRequestHandler
|
||
"textDocument/hover"
|
||
HoverParams
|
||
(Option Hover)
|
||
handleHover
|
||
registerLspRequestHandler
|
||
"textDocument/declaration"
|
||
TextDocumentPositionParams
|
||
(Array LocationLink)
|
||
(handleDefinition GoToKind.declaration)
|
||
registerLspRequestHandler
|
||
"textDocument/definition"
|
||
TextDocumentPositionParams
|
||
(Array LocationLink)
|
||
(handleDefinition GoToKind.definition)
|
||
registerLspRequestHandler
|
||
"textDocument/typeDefinition"
|
||
TextDocumentPositionParams
|
||
(Array LocationLink)
|
||
(handleDefinition GoToKind.type)
|
||
registerLspRequestHandler
|
||
"textDocument/documentHighlight"
|
||
DocumentHighlightParams
|
||
DocumentHighlightResult
|
||
handleDocumentHighlight
|
||
registerLspRequestHandler
|
||
"textDocument/documentSymbol"
|
||
DocumentSymbolParams
|
||
DocumentSymbolResult
|
||
handleDocumentSymbol
|
||
registerLspRequestHandler
|
||
"textDocument/foldingRange"
|
||
FoldingRangeParams
|
||
(Array FoldingRange)
|
||
handleFoldingRange
|
||
registerLspRequestHandler
|
||
"$/lean/plainGoal"
|
||
PlainGoalParams
|
||
(Option PlainGoal)
|
||
handlePlainGoal
|
||
registerLspRequestHandler
|
||
"$/lean/plainTermGoal"
|
||
PlainTermGoalParams
|
||
(Option PlainTermGoal)
|
||
handlePlainTermGoal
|
||
|
||
end Lean.Server.FileWorker
|