This PR improves the 'Go to Definition' UX, specifically: - Using 'Go to Definition' on a type class projection will now extract the specific instances that were involved and provide them as locations to jump to. For example, using 'Go to Definition' on the `toString` of `toString 0` will yield results for `ToString.toString` and `ToString Nat`. - Using 'Go to Definition' on a macro that produces syntax with type class projections will now also extract the specific instances that were involved and provide them as locations to jump to. For example, using 'Go to Definition' on the `+` of `1 + 1` will yield results for `HAdd.hAdd`, `HAdd α α α` and `Add Nat`. - Using 'Go to Declaration' will now provide all the results of 'Go to Definition' in addition to the elaborator and the parser that were involved. For example, using 'Go to Declaration' on the `+` of `1 + 1` will yield results for `HAdd.hAdd`, `HAdd α α α`, `Add Nat`, ``macro_rules | `($x + $y) => ...`` and `infixl:65 " + " => HAdd.hAdd`. - Using 'Go to Type Definition' on a value with a type that contains multiple constants will now provide 'Go to Definition' results for each constant. For example, using 'Go to Type Definition' on `x` for `x : Array Nat` will yield results for `Array` and `Nat`. ### Details 'Go to Definition' for type class projections was first implemented by #1767, but there were still a couple of shortcomings with the implementation. E.g. in order to jump to the instance in `toString 0`, one had to add another space within the application and then use 'Go to Definition' on that, or macros would block instances from being displayed. Then, when the .ilean format was added, most 'Go to Definition' requests were already handled using the .ileans in the watchdog process, and so the file worker never received them to handle them with the semantic information that it has available. This PR resolves most of the issues with the previous implementation and refactors the 'Go to Definition' control flow so that 'Go to Definition' requests are always handled by the file worker, with the watchdog merely using its .ilean position information to update the positions in the response to a more up-to-date state. This is necessary because the file worker obtains its position information from the .oleans, which need to be rebuilt in order to be up-to-date, while the watchdog always receives .ilean update notifications from each active file worker with the current position information in the editor. Finally, all of the 'Go to Definition' code is refactored to be easier to maintain. ### Breaking changes `InfoTree.hoverableInfoAt?` has been generalized to `InfoTree.hoverableInfoAtM?` and now takes a general `filter` argument instead of several boolean flags, as was the case before.
328 lines
10 KiB
Text
328 lines
10 KiB
Text
/-
|
||
Copyright (c) 2022 Sebastian Ullrich. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Sebastian Ullrich, Lars König, Wojciech Nawrocki
|
||
-/
|
||
prelude
|
||
import Lean.Data.Json.FromToJson.Basic
|
||
import Lean.Util.Path
|
||
import Lean.Server.Utils
|
||
import Lean.Data.Lsp.Internal
|
||
import Lean.Util.CollectFVars
|
||
import Lean.Util.ForEachExpr
|
||
|
||
namespace Lean.Server
|
||
|
||
open Lsp
|
||
open Elab
|
||
|
||
inductive GoToKind
|
||
| declaration | definition | type
|
||
deriving BEq, ToJson, FromJson
|
||
|
||
def GoToKind.determineTargetExprs (kind : GoToKind) (ti : TermInfo) : MetaM (Array Expr) := do
|
||
match kind with
|
||
| .type =>
|
||
let type ← instantiateMVars <| ← Meta.inferType ti.expr
|
||
let (_, targetExprs) ← StateT.run (s := #[]) <| type.forEach fun e =>
|
||
match e with
|
||
| .fvar ..
|
||
| .const .. =>
|
||
modify (·.push e)
|
||
| _ =>
|
||
pure ()
|
||
return targetExprs
|
||
| _ =>
|
||
return #[← instantiateMVars ti.expr]
|
||
|
||
def getInstanceProjectionArg? (e : Expr) : MetaM (Option Expr) := do
|
||
let env ← getEnv
|
||
let .const n _ := e.getAppFn'
|
||
| return none
|
||
let some projInfo := env.getProjectionFnInfo? n
|
||
| return none
|
||
let instIdx := projInfo.numParams
|
||
let appArgs := e.getAppArgs
|
||
return appArgs[instIdx]?
|
||
|
||
def isInstanceProjection (e : Expr) : MetaM Bool := do
|
||
return (← getInstanceProjectionArg? e).isSome
|
||
|
||
/-- Checks whether `ti1` is an instance projection info that subsumes `ti2`. -/
|
||
def isInstanceProjectionInfoFor (kind : GoToKind) (ti1 ti2 : TermInfo) : MetaM Bool := do
|
||
if kind == .type then
|
||
return false
|
||
let some startPos1 := ti1.stx.getPos? (canonicalOnly := true)
|
||
| return false
|
||
let some startPos2 := ti2.stx.getPos? (canonicalOnly := true)
|
||
| return false
|
||
if startPos1 != startPos2 then
|
||
return false
|
||
let e1 ← instantiateMVars ti1.expr
|
||
let e2 ← instantiateMVars ti2.expr
|
||
if ! (← isInstanceProjection e1) || (← isInstanceProjection e2) then
|
||
return false
|
||
return e1.getAppFn' == e2.getAppFn'
|
||
|
||
structure GoToContext where
|
||
doc : DocumentMeta
|
||
kind : GoToKind
|
||
infoTree? : Option InfoTree
|
||
originInfo? : Option Info
|
||
children : PersistentArray InfoTree
|
||
|
||
abbrev GoToM α := ReaderT GoToContext MetaM α
|
||
|
||
def GoToM.run (ctx : GoToContext) (ci : ContextInfo) (lctx : LocalContext) (act : GoToM α) :
|
||
IO α :=
|
||
ci.runMetaM lctx <| ReaderT.run act ctx
|
||
|
||
def locationLinksFromDecl (declName : Name) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
-- Potentially this name is a builtin that has not been imported yet:
|
||
if ! (← getEnv).contains declName then
|
||
return #[]
|
||
let some (declModName, declModUri) ← declMod?
|
||
| return #[]
|
||
let some ranges ← findDeclarationRanges? declName
|
||
| return #[]
|
||
let originSelectionRange? := do
|
||
let i ← ctx.originInfo?
|
||
let r ← i.range?
|
||
return r.toLspRange ctx.doc.text
|
||
let ll : LeanLocationLink := {
|
||
originSelectionRange?
|
||
targetUri := declModUri
|
||
targetRange := ranges.range.toLspRange
|
||
targetSelectionRange := ranges.selectionRange.toLspRange
|
||
ident? := some ⟨declModName, declName.eraseMacroScopes⟩
|
||
isDefault := false
|
||
}
|
||
return #[ll]
|
||
where
|
||
declMod? : GoToM (Option (Name × DocumentUri)) := do
|
||
let ctx ← read
|
||
let declMod? ← findModuleOf? declName
|
||
match declMod? with
|
||
| some declModName =>
|
||
let some declModUri ← documentUriFromModule? declModName
|
||
| return none
|
||
return some (declModName, declModUri)
|
||
| none =>
|
||
return some (ctx.doc.mod, ctx.doc.uri)
|
||
|
||
def locationLinksFromBinder (id : FVarId) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
let doc := ctx.doc
|
||
let some binderInfo ← binderInfo?
|
||
| return #[]
|
||
let some binderInfoRange := binderInfo.range?
|
||
| return #[]
|
||
let binderInfoRange := binderInfoRange.toLspRange doc.text
|
||
let originSelectionRange? := do
|
||
let i ← ctx.originInfo?
|
||
let r ← i.range?
|
||
return r.toLspRange doc.text
|
||
let ll : LeanLocationLink := {
|
||
originSelectionRange?
|
||
targetUri := doc.uri
|
||
targetRange := binderInfoRange
|
||
targetSelectionRange := binderInfoRange
|
||
ident? := none
|
||
isDefault := false
|
||
}
|
||
return #[ll]
|
||
where
|
||
binderInfo? : GoToM (Option Info) := do
|
||
let ctx ← read
|
||
let some infoTree := ctx.infoTree?
|
||
| return none
|
||
return infoTree.findInfo? fun
|
||
| .ofTermInfo { isBinder := true, expr := .fvar id' .., .. } => id' == id
|
||
| _ => false
|
||
|
||
def locationLinksFromImport (i : CommandInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
let `(Parser.Module.import| $[public]? $[meta]? import $[all]? $mod) := i.stx
|
||
| return #[]
|
||
let some modUri ← documentUriFromModule? mod.getId
|
||
| return #[]
|
||
let range := { start := ⟨0, 0⟩, «end» := ⟨0, 0⟩ : Range }
|
||
let originSelectionRange? := do
|
||
let r ← mod.raw.getRange? (canonicalOnly := true)
|
||
return r.toLspRange ctx.doc.text
|
||
let ll : LeanLocationLink := {
|
||
originSelectionRange?
|
||
targetUri := modUri
|
||
targetRange := range
|
||
targetSelectionRange := range
|
||
ident? := none
|
||
isDefault := false
|
||
}
|
||
return #[ll]
|
||
|
||
def locationLinksDefault : GoToM (Array LeanLocationLink) := do
|
||
-- If other location link resolutions fail, we try to show the elaborator or parser
|
||
let names ← defaultDeclNames
|
||
let mut ll := #[]
|
||
for name in names do
|
||
ll := ll ++ (← locationLinksFromDecl name)
|
||
return ll.map ({· with isDefault := true})
|
||
where
|
||
defaultDeclNames : GoToM (Array Name) := do
|
||
let ctx ← read
|
||
let env ← getEnv
|
||
let some originInfo := ctx.originInfo?
|
||
| return #[]
|
||
let some ei := originInfo.toElabInfo?
|
||
| return #[]
|
||
let e := ei.elaborator
|
||
if e == `Delab then
|
||
return #[]
|
||
if e == `Lean.Elab.Term.elabApp || e == `Lean.Elab.Term.elabIdent then
|
||
return #[]
|
||
let mut names := #[]
|
||
if env.contains e then
|
||
names := names.push e
|
||
if ctx.kind == .declaration && env.contains ei.stx.getKind then
|
||
names := names.push ei.stx.getKind
|
||
return names
|
||
|
||
def locationLinksFromErrorNameInfo (eni : ErrorNameInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
let some explan := getErrorExplanationRaw? (← getEnv) eni.errorName
|
||
| return #[]
|
||
let some loc := explan.declLoc?
|
||
| return #[]
|
||
let some modUri ← documentUriFromModule? loc.module
|
||
| return #[]
|
||
let range := loc.range.toLspRange
|
||
let originSelectionRange? := do
|
||
let r ← eni.stx.getRange? (canonicalOnly := true)
|
||
return r.toLspRange ctx.doc.text
|
||
let link : LeanLocationLink := {
|
||
originSelectionRange?
|
||
targetUri := modUri
|
||
targetRange := range
|
||
targetSelectionRange := range
|
||
ident? := none
|
||
isDefault := false
|
||
}
|
||
return #[link]
|
||
|
||
def locationLinksFromInstanceProjection (e : Expr) : GoToM (Array LeanLocationLink) := do
|
||
-- Go-to-definition on a projection application of a typeclass
|
||
-- should return all instances generated by TC.
|
||
let .const n _ := e.getAppFn.consumeMData
|
||
| return #[]
|
||
let some instArg ← getInstanceProjectionArg? e
|
||
| return #[]
|
||
let mut results := #[]
|
||
for inst in ← extractInstances instArg do
|
||
results := results.append <| ← locationLinksFromDecl inst
|
||
results := results.append <| ← locationLinksFromDecl n
|
||
return results
|
||
where
|
||
extractInstances (e : Expr) : GoToM (Array Name) := do
|
||
match e with
|
||
| .const declName _ =>
|
||
if ! (← Lean.Meta.isInstance declName) then
|
||
return #[]
|
||
return #[declName]
|
||
| .app fn arg =>
|
||
let fnInstances ← extractInstances fn
|
||
let argInstances ← extractInstances arg
|
||
return argInstances ++ fnInstances
|
||
| .mdata _ e =>
|
||
extractInstances e
|
||
| _ =>
|
||
return #[]
|
||
|
||
def locationLinksFromTermInfo (ti : TermInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
let mut ll := #[]
|
||
let exprs ← ctx.kind.determineTargetExprs ti
|
||
for expr in exprs do
|
||
let newLL ←
|
||
match expr.consumeMData with
|
||
| Expr.const n .. =>
|
||
locationLinksFromDecl n
|
||
| Expr.fvar id .. =>
|
||
locationLinksFromBinder id
|
||
| _ =>
|
||
locationLinksFromInstanceProjection expr
|
||
ll := ll ++ newLL
|
||
return ll
|
||
|
||
def locationLinksFromDelabTermInfo (dti : DelabTermInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
let { toTermInfo := ti, location?, .. } := dti
|
||
let some location := location?
|
||
| return ← locationLinksFromTermInfo ti
|
||
let some targetUri ← documentUriFromModule? location.module
|
||
-- If we fail to find a DocumentUri, use the term info so that we have something to jump to.
|
||
| return ← locationLinksFromTermInfo ti
|
||
let range := location.range.toLspRange
|
||
let originSelectionRange? := do
|
||
let r ← Info.ofDelabTermInfo dti |>.range?
|
||
return r.toLspRange ctx.doc.text
|
||
let result : LeanLocationLink := {
|
||
originSelectionRange?
|
||
targetUri
|
||
targetRange := range
|
||
targetSelectionRange := range
|
||
ident? := none
|
||
isDefault := false
|
||
}
|
||
return #[result]
|
||
|
||
def locationLinksFromFieldInfo (fi : FieldInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
if ctx.kind != .type then
|
||
return ← locationLinksFromDecl fi.projName
|
||
let expr ← instantiateMVars (← Meta.inferType fi.val)
|
||
let some n := expr.getAppFn.constName?
|
||
| return #[]
|
||
locationLinksFromDecl n
|
||
|
||
def locationLinksFromOptionInfo (i : OptionInfo) : GoToM (Array LeanLocationLink) :=
|
||
locationLinksFromDecl i.declName
|
||
|
||
def locationLinksFromCommandInfo (i : CommandInfo) : GoToM (Array LeanLocationLink) := do
|
||
let ctx ← read
|
||
if ! (i matches ⟨`import, _⟩) || ctx.kind == .type then
|
||
return #[]
|
||
locationLinksFromImport i
|
||
|
||
def locationLinksOfInfo (doc : DocumentMeta) (kind : GoToKind) (ictx : InfoWithCtx)
|
||
(infoTree? : Option InfoTree := none) : IO (Array LeanLocationLink) := do
|
||
let ctx : GoToContext := {
|
||
doc
|
||
kind
|
||
infoTree?
|
||
originInfo? := some ictx.info
|
||
children := ictx.children
|
||
}
|
||
GoToM.run ctx ictx.ctx ictx.info.lctx do
|
||
let ll ←
|
||
match ictx.info with
|
||
| .ofTermInfo ti =>
|
||
locationLinksFromTermInfo ti
|
||
| .ofDelabTermInfo dti =>
|
||
locationLinksFromDelabTermInfo dti
|
||
| .ofFieldInfo fi =>
|
||
locationLinksFromFieldInfo fi
|
||
| .ofOptionInfo oi =>
|
||
locationLinksFromOptionInfo oi
|
||
| .ofCommandInfo cci =>
|
||
locationLinksFromCommandInfo cci
|
||
| .ofErrorNameInfo eni =>
|
||
locationLinksFromErrorNameInfo eni
|
||
| _ =>
|
||
pure #[]
|
||
if kind == .declaration || ll.isEmpty then
|
||
return ll ++ (← locationLinksDefault)
|
||
return ll
|
||
|
||
end Lean.Server
|