This PR adds support for the "call hierarchy" feature of LSP that allows quickly navigating both inbound and outbound call sites of functions. In this PR, "call" is taken to mean "usage", so inbound and outbound references of all kinds of identifiers (e.g. functions or types) can be navigated. To implement the call hierarchy feature, this PR implements the LSP requests `textDocument/prepareCallHierarchy`, `callHierarchy/incomingCalls` and `callHierarchy/outgoingCalls`. <details> <summary>Showing the call hierarchy (click to show image)</summary>  </details> <details> <summary>Incoming calls (click to show image)</summary>  </details> <details> <summary>Outgoing calls (click to show image)</summary>  </details> It is based on #3159, which should be merged before this PR. To route the parent declaration name through to the language server, the `.ilean` format is adjusted, breaking backwards compatibility with version 1 of the ILean format and yielding version 2. This PR also makes the following more minor adjustments: - `Lean.Server.findModuleRefs` now also combines the identifiers of constants and FVars and prefers constant over FVars for the combined identifier. This is necessary because e.g. declarations declared using `where` yield both a constant (for usage outside of the function) and an FVar (for usage inside of the function) with the same range, whereas we would typically like all references to refer to the former. This also fixes a bug introduced in #2462 where renaming a declaration declared using `where` would not rename usages outside of the function, as well as a bug in the unused variable linter where `where` declarations would be reported as unused even if they were being used outside of the function. - The function converting `Lean.Server.RefInfo` to `Lean.Lsp.RefInfo` now also computes the `Lean.DeclarationRanges` for parent declaration names via `MetaM` and must hence be in `IO` now. - Add a utility function `Array.groupByKey` to `HashMap.lean`. - Stylistic refactoring of `Watchdog.lean` and `LanguageFeatures.lean`.
399 lines
16 KiB
Text
399 lines
16 KiB
Text
/-
|
||
Copyright (c) 2021 Joscha Mennicken. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
|
||
Authors: Joscha Mennicken
|
||
-/
|
||
import Lean.Data.Lsp.Internal
|
||
import Lean.Server.Utils
|
||
|
||
/-! # Representing collected and deduplicated definitions and usages -/
|
||
|
||
namespace Lean.Server
|
||
open Lsp Lean.Elab Std
|
||
|
||
structure Reference where
|
||
ident : RefIdent
|
||
/-- FVarIds that are logically identical to this reference -/
|
||
aliases : Array RefIdent := #[]
|
||
range : Lsp.Range
|
||
stx : Syntax
|
||
ci : ContextInfo
|
||
info : Info
|
||
isBinder : Bool
|
||
|
||
structure RefInfo where
|
||
definition : Option Reference
|
||
usages : Array Reference
|
||
|
||
namespace RefInfo
|
||
|
||
def empty : RefInfo := ⟨ none, #[] ⟩
|
||
|
||
def addRef : RefInfo → Reference → RefInfo
|
||
| i@{ definition := none, .. }, ref@{ isBinder := true, .. } =>
|
||
{ i with definition := ref }
|
||
| i@{ usages, .. }, ref@{ isBinder := false, .. } =>
|
||
{ i with usages := usages.push ref }
|
||
| i, _ => i
|
||
|
||
def toLspRefInfo (i : RefInfo) : IO Lsp.RefInfo := do
|
||
let refToRefInfoLocation (ref : Reference) : IO RefInfo.Location := do
|
||
let parentDeclName? := ref.ci.parentDecl?
|
||
let parentDeclRanges? ← ref.ci.runMetaM ref.info.lctx do
|
||
let some parentDeclName := parentDeclName?
|
||
| return none
|
||
findDeclarationRanges? parentDeclName
|
||
return {
|
||
range := ref.range
|
||
parentDecl? := do
|
||
let parentDeclName ← parentDeclName?
|
||
let parentDeclRange := (← parentDeclRanges?).range.toLspRange
|
||
let parentDeclSelectionRange := (← parentDeclRanges?).selectionRange.toLspRange
|
||
return ⟨parentDeclName, parentDeclRange, parentDeclSelectionRange⟩
|
||
}
|
||
let definition? ← i.definition.mapM refToRefInfoLocation
|
||
let usages ← i.usages.mapM refToRefInfoLocation
|
||
return {
|
||
definition? := definition?
|
||
usages := usages
|
||
}
|
||
|
||
end RefInfo
|
||
|
||
def ModuleRefs := HashMap RefIdent RefInfo
|
||
|
||
namespace ModuleRefs
|
||
|
||
def addRef (self : ModuleRefs) (ref : Reference) : ModuleRefs :=
|
||
let refInfo := self.findD ref.ident RefInfo.empty
|
||
self.insert ref.ident (refInfo.addRef ref)
|
||
|
||
def toLspModuleRefs (refs : ModuleRefs) : IO Lsp.ModuleRefs := do
|
||
let refs ← refs.toList.mapM fun (k, v) => do
|
||
return (k, ← v.toLspRefInfo)
|
||
return HashMap.ofList refs
|
||
|
||
end ModuleRefs
|
||
|
||
end Lean.Server
|
||
|
||
namespace Lean.Lsp.RefInfo
|
||
open Server
|
||
|
||
def empty : RefInfo := ⟨ none, #[] ⟩
|
||
|
||
def merge (a : RefInfo) (b : RefInfo) : RefInfo :=
|
||
{
|
||
definition? := b.definition?.orElse fun _ => a.definition?
|
||
usages := a.usages.append b.usages
|
||
}
|
||
|
||
def findRange? (self : RefInfo) (pos : Lsp.Position) (includeStop := false) : Option Range := do
|
||
if let some ⟨range, _⟩ := self.definition? then
|
||
if contains range pos then
|
||
return range
|
||
for ⟨range, _⟩ in self.usages do
|
||
if contains range pos then
|
||
return range
|
||
none
|
||
where
|
||
contains (range : Lsp.Range) (pos : Lsp.Position) : Bool :=
|
||
-- Note: includeStop is used here to toggle between closed-interval and half-open-interval
|
||
-- behavior for the range. Closed-interval behavior matches the expectation of VSCode
|
||
-- when selecting an identifier at a cursor position, see #767.
|
||
range.start <= pos && (if includeStop then pos <= range.end else pos < range.end)
|
||
|
||
def contains (self : RefInfo) (pos : Lsp.Position) (includeStop := false) : Bool := Id.run do
|
||
(self.findRange? pos includeStop).isSome
|
||
|
||
end Lean.Lsp.RefInfo
|
||
|
||
namespace Lean.Lsp.ModuleRefs
|
||
open Server
|
||
|
||
def findAt (self : ModuleRefs) (pos : Lsp.Position) (includeStop := false) : Array RefIdent := Id.run do
|
||
let mut result := #[]
|
||
for (ident, info) in self.toList do
|
||
if info.contains pos includeStop then
|
||
result := result.push ident
|
||
result
|
||
|
||
def findRange? (self : ModuleRefs) (pos : Lsp.Position) (includeStop := false) : Option Range := do
|
||
for (_, info) in self.toList do
|
||
if let some range := info.findRange? pos includeStop then
|
||
return range
|
||
none
|
||
|
||
end Lean.Lsp.ModuleRefs
|
||
|
||
namespace Lean.Server
|
||
open IO
|
||
open Lsp
|
||
open Elab
|
||
|
||
/-- Content of individual `.ilean` files -/
|
||
structure Ilean where
|
||
version : Nat := 2
|
||
module : Name
|
||
references : Lsp.ModuleRefs
|
||
deriving FromJson, ToJson
|
||
|
||
namespace Ilean
|
||
|
||
def load (path : System.FilePath) : IO Ilean := do
|
||
let content ← FS.readFile path
|
||
match Json.parse content >>= fromJson? with
|
||
| Except.ok ilean => pure ilean
|
||
| Except.error msg => throwServerError s!"Failed to load ilean at {path}: {msg}"
|
||
|
||
end Ilean
|
||
/-! # Collecting and deduplicating definitions and usages -/
|
||
|
||
def identOf : Info → Option (RefIdent × Bool)
|
||
| Info.ofTermInfo ti => match ti.expr with
|
||
| Expr.const n .. => some (RefIdent.const n, ti.isBinder)
|
||
| Expr.fvar id .. => some (RefIdent.fvar id, ti.isBinder)
|
||
| _ => none
|
||
| Info.ofFieldInfo fi => some (RefIdent.const fi.projName, false)
|
||
| Info.ofOptionInfo oi => some (RefIdent.const oi.declName, false)
|
||
| _ => none
|
||
|
||
def findReferences (text : FileMap) (trees : Array InfoTree) : Array Reference := Id.run <| StateT.run' (s := #[]) do
|
||
for tree in trees do
|
||
tree.visitM' (postNode := fun ci info _ => do
|
||
if let some (ident, isBinder) := identOf info then
|
||
if let some range := info.range? then
|
||
if info.stx.getHeadInfo matches .original .. then -- we are not interested in canonical syntax here
|
||
modify (·.push { ident, range := range.toLspRange text, stx := info.stx, ci, info, isBinder }))
|
||
get
|
||
|
||
/--
|
||
There are several different identifiers that should be considered equal for the purpose of finding
|
||
all references of an identifier:
|
||
- `FVarId`s of a function parameter in the function's signature and body
|
||
- Chains of helper definitions like those created for do-reassignment `x := e`
|
||
- Overlapping definitions like those defined by `where` declarations that define both an FVar
|
||
(for local usage) and a constant (for non-local usage)
|
||
- Identifiers connected by `FVarAliasInfo` such as variables before and after `match` generalization
|
||
|
||
In the first three cases that are not explicitly denoted as aliases with an `FVarAliasInfo`, the
|
||
corresponding `Reference`s have the exact same range.
|
||
This function finds all definitions that have the exact same range as another definition or usage
|
||
and collapses them into a single identifier. It also collapses identifiers connected by
|
||
an `FVarAliasInfo`.
|
||
When collapsing identifiers, it prefers using a `RefIdent.const name` over a `RefIdent.fvar id` for
|
||
all identifiers that are being collapsed into one.
|
||
-/
|
||
partial def combineIdents (trees : Array InfoTree) (refs : Array Reference) : Array Reference := Id.run do
|
||
-- Deduplicate definitions based on their exact range
|
||
let mut posMap : HashMap Lsp.Range RefIdent := HashMap.empty
|
||
for ref in refs do
|
||
if let { ident, range, isBinder := true, .. } := ref then
|
||
posMap := posMap.insert range ident
|
||
|
||
let idMap := useConstRepresentatives <| buildIdMap posMap
|
||
|
||
let mut refs' := #[]
|
||
for ref in refs do
|
||
let id := ref.ident
|
||
if idMap.contains id then
|
||
refs' := refs'.push { ref with ident := findCanonicalRepresentative idMap id, aliases := #[id] }
|
||
else if !idMap.contains id then
|
||
refs' := refs'.push ref
|
||
refs'
|
||
where
|
||
useConstRepresentatives (idMap : HashMap RefIdent RefIdent)
|
||
: HashMap RefIdent RefIdent := Id.run do
|
||
let insertIntoClass classesById id :=
|
||
let representative := findCanonicalRepresentative idMap id
|
||
let «class» := classesById.findD representative ∅
|
||
let classesById := classesById.erase representative -- make `«class»` referentially unique
|
||
let «class» := «class».insert id
|
||
classesById.insert representative «class»
|
||
|
||
-- collect equivalence classes
|
||
let mut classesById : HashMap RefIdent (HashSet RefIdent) := ∅
|
||
for ⟨id, baseId⟩ in idMap.toArray do
|
||
classesById := insertIntoClass classesById id
|
||
classesById := insertIntoClass classesById baseId
|
||
|
||
let mut r := ∅
|
||
for ⟨currentRepresentative, «class»⟩ in classesById.toArray do
|
||
-- find best representative (ideally a const if available)
|
||
let mut bestRepresentative := currentRepresentative
|
||
for id in «class» do
|
||
bestRepresentative :=
|
||
match bestRepresentative, id with
|
||
| .fvar a, .fvar _ => .fvar a
|
||
| .fvar _, .const b => .const b
|
||
| .const a, .fvar _ => .const a
|
||
| .const a, .const _ => .const a
|
||
|
||
-- compress `idMap` so that all identifiers in a class point to the best representative
|
||
for id in «class» do
|
||
if id != bestRepresentative then
|
||
r := r.insert id bestRepresentative
|
||
return r
|
||
|
||
findCanonicalRepresentative (idMap : HashMap RefIdent RefIdent) (id : RefIdent) : RefIdent := Id.run do
|
||
let mut canonicalRepresentative := id
|
||
while idMap.contains canonicalRepresentative do
|
||
canonicalRepresentative := idMap.find! canonicalRepresentative
|
||
return canonicalRepresentative
|
||
|
||
buildIdMap posMap := Id.run <| StateT.run' (s := HashMap.empty) do
|
||
-- map fvar defs to overlapping fvar defs/uses
|
||
for ref in refs do
|
||
let baseId := ref.ident
|
||
if let some id := posMap.find? ref.range then
|
||
insertIdMap id baseId
|
||
|
||
-- apply `FVarAliasInfo`
|
||
trees.forM (·.visitM' (postNode := fun _ info _ => do
|
||
if let .ofFVarAliasInfo ai := info then
|
||
insertIdMap (.fvar ai.id) (.fvar ai.baseId)))
|
||
|
||
get
|
||
|
||
-- poor man's union-find; see also `findCanonicalBinder`
|
||
insertIdMap id baseId := do
|
||
let idMap ← get
|
||
let id := findCanonicalRepresentative idMap id
|
||
let baseId := findCanonicalRepresentative idMap baseId
|
||
if baseId != id then
|
||
modify (·.insert id baseId)
|
||
|
||
def dedupReferences (refs : Array Reference) (allowSimultaneousBinderUse := false) : Array Reference := Id.run do
|
||
let mut refsByIdAndRange : HashMap (RefIdent × Option Bool × Lsp.Range) Reference := HashMap.empty
|
||
for ref in refs do
|
||
let isBinder := if allowSimultaneousBinderUse then some ref.isBinder else none
|
||
let key := (ref.ident, isBinder, ref.range)
|
||
refsByIdAndRange := match refsByIdAndRange[key] with
|
||
| some ref' => refsByIdAndRange.insert key { ref' with aliases := ref'.aliases ++ ref.aliases }
|
||
| none => refsByIdAndRange.insert key ref
|
||
|
||
let dedupedRefs := refsByIdAndRange.fold (init := #[]) fun refs _ ref => refs.push ref
|
||
return dedupedRefs.qsort (·.range < ·.range)
|
||
|
||
def findModuleRefs (text : FileMap) (trees : Array InfoTree) (localVars : Bool := true)
|
||
(allowSimultaneousBinderUse := false) : ModuleRefs := Id.run do
|
||
let mut refs :=
|
||
dedupReferences (allowSimultaneousBinderUse := allowSimultaneousBinderUse) <|
|
||
combineIdents trees <|
|
||
findReferences text trees
|
||
if !localVars then
|
||
refs := refs.filter fun
|
||
| { ident := RefIdent.fvar _, .. } => false
|
||
| _ => true
|
||
refs.foldl (init := HashMap.empty) fun m ref => m.addRef ref
|
||
|
||
/-! # Collecting and maintaining reference info from different sources -/
|
||
|
||
structure References where
|
||
/-- References loaded from ilean files -/
|
||
ileans : HashMap Name (System.FilePath × Lsp.ModuleRefs)
|
||
/-- References from workers, overriding the corresponding ilean files -/
|
||
workers : HashMap Name (Nat × Lsp.ModuleRefs)
|
||
|
||
namespace References
|
||
|
||
def empty : References := { ileans := HashMap.empty, workers := HashMap.empty }
|
||
|
||
def addIlean (self : References) (path : System.FilePath) (ilean : Ilean) : References :=
|
||
{ self with ileans := self.ileans.insert ilean.module (path, ilean.references) }
|
||
|
||
def removeIlean (self : References) (path : System.FilePath) : References :=
|
||
let namesToRemove := self.ileans.toList.filter (fun (_, p, _) => p == path)
|
||
|>.map (fun (n, _, _) => n)
|
||
namesToRemove.foldl (init := self) fun self name =>
|
||
{ self with ileans := self.ileans.erase name }
|
||
|
||
def updateWorkerRefs (self : References) (name : Name) (version : Nat) (refs : Lsp.ModuleRefs) : References := Id.run do
|
||
if let some (currVersion, _) := self.workers.find? name then
|
||
if version > currVersion then
|
||
return { self with workers := self.workers.insert name (version, refs) }
|
||
if version == currVersion then
|
||
let current := self.workers.findD name (version, HashMap.empty)
|
||
let merged := refs.fold (init := current.snd) fun m ident info =>
|
||
m.findD ident Lsp.RefInfo.empty |>.merge info |> m.insert ident
|
||
return { self with workers := self.workers.insert name (version, merged) }
|
||
return self
|
||
|
||
def finalizeWorkerRefs (self : References) (name : Name) (version : Nat) (refs : Lsp.ModuleRefs) : References := Id.run do
|
||
if let some (currVersion, _) := self.workers.find? name then
|
||
if version < currVersion then
|
||
return self
|
||
return { self with workers := self.workers.insert name (version, refs) }
|
||
|
||
def removeWorkerRefs (self : References) (name : Name) : References :=
|
||
{ self with workers := self.workers.erase name }
|
||
|
||
def allRefs (self : References) : HashMap Name Lsp.ModuleRefs :=
|
||
let ileanRefs := self.ileans.toList.foldl (init := HashMap.empty) fun m (name, _, refs) => m.insert name refs
|
||
self.workers.toList.foldl (init := ileanRefs) fun m (name, _, refs) => m.insert name refs
|
||
|
||
def findAt (self : References) (module : Name) (pos : Lsp.Position) (includeStop := false) : Array RefIdent := Id.run do
|
||
if let some refs := self.allRefs.find? module then
|
||
return refs.findAt pos includeStop
|
||
#[]
|
||
|
||
def findRange? (self : References) (module : Name) (pos : Lsp.Position) (includeStop := false) : Option Range := do
|
||
let refs ← self.allRefs.find? module
|
||
refs.findRange? pos includeStop
|
||
|
||
structure DocumentRefInfo where
|
||
location : Location
|
||
parentInfo? : Option RefInfo.ParentDecl
|
||
|
||
def referringTo (self : References) (identModule : Name) (ident : RefIdent) (srcSearchPath : SearchPath)
|
||
(includeDefinition : Bool := true) : IO (Array DocumentRefInfo) := do
|
||
let refsToCheck := match ident with
|
||
| RefIdent.const _ => self.allRefs.toList
|
||
| RefIdent.fvar _ => match self.allRefs.find? identModule with
|
||
| none => []
|
||
| some refs => [(identModule, refs)]
|
||
let mut result := #[]
|
||
for (module, refs) in refsToCheck do
|
||
if let some info := refs.find? ident then
|
||
if let some path ← srcSearchPath.findModuleWithExt "lean" module then
|
||
-- Resolve symlinks (such as `src` in the build dir) so that files are
|
||
-- opened in the right folder
|
||
let uri := System.Uri.pathToUri <| ← IO.FS.realPath path
|
||
if includeDefinition then
|
||
if let some ⟨range, parentDeclInfo?⟩ := info.definition? then
|
||
result := result.push ⟨⟨uri, range⟩, parentDeclInfo?⟩
|
||
for ⟨range, parentDeclInfo?⟩ in info.usages do
|
||
result := result.push ⟨⟨uri, range⟩, parentDeclInfo?⟩
|
||
return result
|
||
|
||
def definitionOf? (self : References) (ident : RefIdent) (srcSearchPath : SearchPath)
|
||
: IO (Option DocumentRefInfo) := do
|
||
for (module, refs) in self.allRefs.toList do
|
||
if let some info := refs.find? ident then
|
||
if let some ⟨definitionRange, definitionParentDeclInfo?⟩ := info.definition? then
|
||
if let some path ← srcSearchPath.findModuleWithExt "lean" module then
|
||
-- Resolve symlinks (such as `src` in the build dir) so that files are
|
||
-- opened in the right folder
|
||
let uri := System.Uri.pathToUri <| ← IO.FS.realPath path
|
||
return some ⟨⟨uri, definitionRange⟩, definitionParentDeclInfo?⟩
|
||
return none
|
||
|
||
def definitionsMatching (self : References) (srcSearchPath : SearchPath) (filter : Name → Option α)
|
||
(maxAmount? : Option Nat := none) : IO $ Array (α × Location) := do
|
||
let mut result := #[]
|
||
for (module, refs) in self.allRefs.toList do
|
||
if let some path ← srcSearchPath.findModuleWithExt "lean" module then
|
||
let uri := System.Uri.pathToUri <| ← IO.FS.realPath path
|
||
for (ident, info) in refs.toList do
|
||
if let (RefIdent.const name, some ⟨definitionRange, _⟩) := (ident, info.definition?) then
|
||
if let some a := filter name then
|
||
result := result.push (a, ⟨uri, definitionRange⟩)
|
||
if let some maxAmount := maxAmount? then
|
||
if result.size >= maxAmount then
|
||
return result
|
||
return result
|
||
|
||
end References
|
||
|
||
end Lean.Server
|