blob: a9f52e24729bd313f7f01a750683cf37b2a97cfa [file]
/-
Copyright (c) 2021 Wojciech Nawrocki. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Wojciech Nawrocki
-/
import Lean.PrettyPrinter
namespace Lean.Elab
/-- Elaborator information with elaborator context.
It can be thought of as a "thunked" elaboration computation that allows us
to retroactively extract type information, symbol locations, etc.
through arbitrary invocations of `runMetaM` (where the necessary context and state
can be reconstructed from `ctx` and `info.lctx`).
W.r.t. widgets, this is used to tag different parts of expressions in `ppExprTagged`.
This is the input to the RPC call `Lean.Widget.InteractiveDiagnostics.infoToInteractive`.
It carries over information about delaborated
`Info` nodes in a `CodeWithInfos`, and the associated pretty-printing
functionality is purpose-specific to showing the contents of infoview popups.
For use in standard LSP go-to-definition (see `Lean.Server.FileWorker.locationLinksOfInfo`),
all the elaborator information we need for similar tasks is already fully recoverable via
the `InfoTree` structure (see `Lean.Elab.InfoTree.visitM`).
There we use this as a convienience wrapper for queried nodes (e.g. the return value of
`Lean.Elab.InfoTree.hoverableInfoAt?`). It also includes the children info nodes
as additional context (this is unused in the RPC case, as delaboration has no notion of child nodes).
NOTE: This type is for internal use in the infoview/LSP. It should not be used in user widgets.
-/
structure InfoWithCtx where
ctx : Elab.ContextInfo
info : Elab.Info
children : PersistentArray InfoTree
/-- Visit nodes, passing in a surrounding context (the innermost one) and accumulating results on the way back up. -/
partial def InfoTree.visitM [Monad m]
(preNode : ContextInfo → Info → (children : PersistentArray InfoTree) → m Unit := fun _ _ _ => pure ())
(postNode : ContextInfo → Info → (children : PersistentArray InfoTree) → List (Option α) → m α)
: InfoTree → m (Option α) :=
go none
where go
| _, context ctx t => go ctx t
| some ctx, node i cs => do
preNode ctx i cs
let as ← cs.toList.mapM (go <| i.updateContext? ctx)
postNode ctx i cs as
| none, node .. => panic! "unexpected context-free info tree node"
| _, hole .. => pure none
/-- `InfoTree.visitM` specialized to `Unit` return type -/
def InfoTree.visitM' [Monad m]
(preNode : ContextInfo → Info → (children : PersistentArray InfoTree) → m Unit := fun _ _ _ => pure ())
(postNode : ContextInfo → Info → (children : PersistentArray InfoTree) → m Unit := fun _ _ _ => pure ())
(t : InfoTree) : m Unit := t.visitM preNode (fun ci i cs _ => postNode ci i cs) |> discard
/--
Visit nodes bottom-up, passing in a surrounding context (the innermost one) and the union of nested results (empty at leaves). -/
def InfoTree.collectNodesBottomUp (p : ContextInfo → Info → PersistentArray InfoTree → List α → List α) (i : InfoTree) : List α :=
i.visitM (m := Id) (postNode := fun ci i cs as => p ci i cs (as.filterMap id).join) |>.getD []
/--
For every branch of the `InfoTree`, find the deepest node in that branch for which `p` returns
`some _` and return the union of all such nodes. The visitor `p` is given a node together with
its innermost surrounding `ContextInfo`. -/
partial def InfoTree.deepestNodes (p : ContextInfo → Info → PersistentArray InfoTree → Option α) (infoTree : InfoTree) : List α :=
infoTree.collectNodesBottomUp fun ctx i cs rs =>
if rs.isEmpty then
match p ctx i cs with
| some r => [r]
| none => []
else
rs
partial def InfoTree.foldInfo (f : ContextInfo → Info → α → α) (init : α) : InfoTree → α :=
go none init
where go ctx? a
| context ctx t => go ctx a t
| node i ts =>
let a := match ctx? with
| none => a
| some ctx => f ctx i a
ts.foldl (init := a) (go <| i.updateContext? ctx?)
| _ => a
def Info.isTerm : Info → Bool
| ofTermInfo _ => true
| _ => false
def Info.isCompletion : Info → Bool
| ofCompletionInfo .. => true
| _ => false
def InfoTree.getCompletionInfos (infoTree : InfoTree) : Array (ContextInfo × CompletionInfo) :=
infoTree.foldInfo (init := #[]) fun ctx info result =>
match info with
| Info.ofCompletionInfo info => result.push (ctx, info)
| _ => result
def Info.stx : Info → Syntax
| ofTacticInfo i => i.stx
| ofTermInfo i => i.stx
| ofCommandInfo i => i.stx
| ofMacroExpansionInfo i => i.stx
| ofOptionInfo i => i.stx
| ofFieldInfo i => i.stx
| ofCompletionInfo i => i.stx
| ofCustomInfo i => i.stx
| ofUserWidgetInfo i => i.stx
| ofFVarAliasInfo _ => .missing
| ofFieldRedeclInfo i => i.stx
def Info.lctx : Info → LocalContext
| Info.ofTermInfo i => i.lctx
| Info.ofFieldInfo i => i.lctx
| _ => LocalContext.empty
def Info.pos? (i : Info) : Option String.Pos :=
i.stx.getPos? (canonicalOnly := true)
def Info.tailPos? (i : Info) : Option String.Pos :=
i.stx.getTailPos? (canonicalOnly := true)
def Info.range? (i : Info) : Option String.Range :=
i.stx.getRange? (canonicalOnly := true)
def Info.contains (i : Info) (pos : String.Pos) (includeStop := false) : Bool :=
i.range?.any (·.contains pos includeStop)
def Info.size? (i : Info) : Option String.Pos := do
let pos ← i.pos?
let tailPos ← i.tailPos?
return tailPos - pos
-- `Info` without position information are considered to have "infinite" size
def Info.isSmaller (i₁ i₂ : Info) : Bool :=
match i₁.size?, i₂.pos? with
| some sz₁, some sz₂ => sz₁ < sz₂
| some _, none => true
| _, _ => false
def Info.occursBefore? (i : Info) (hoverPos : String.Pos) : Option String.Pos := do
let tailPos ← i.tailPos?
guard (tailPos ≤ hoverPos)
return hoverPos - tailPos
def Info.occursInside? (i : Info) (hoverPos : String.Pos) : Option String.Pos := do
let headPos ← i.pos?
let tailPos ← i.tailPos?
guard (headPos ≤ hoverPos && hoverPos < tailPos)
return hoverPos - headPos
def InfoTree.smallestInfo? (p : Info → Bool) (t : InfoTree) : Option (ContextInfo × Info) :=
let ts := t.deepestNodes fun ctx i _ => if p i then some (ctx, i) else none
let infos := ts.map fun (ci, i) =>
let diff := i.tailPos?.get! - i.pos?.get!
(diff, ci, i)
infos.toArray.getMax? (fun a b => a.1 > b.1) |>.map fun (_, ci, i) => (ci, i)
/-- Find an info node, if any, which should be shown on hover/cursor at position `hoverPos`. -/
partial def InfoTree.hoverableInfoAt? (t : InfoTree) (hoverPos : String.Pos) (includeStop := false) (omitAppFns := false) (omitIdentApps := false) : Option InfoWithCtx := Id.run do
let results := t.visitM (m := Id) (postNode := fun ctx info children results => do
let mut results := results.bind (·.getD [])
if omitAppFns && info.stx.isOfKind ``Parser.Term.app && info.stx[0].isIdent then
results := results.filter (·.2.info.stx != info.stx[0])
if omitIdentApps && info.stx.isIdent then
-- if an identifier stands for an application (e.g. in the case of a typeclass projection), prefer the application
if let .ofTermInfo ti := info then
if ti.expr.isApp then
results := results.filter (·.2.info.stx != info.stx)
unless results.isEmpty do
return results -- prefer innermost results
/-
Remark: we skip `info` nodes associated with the `nullKind` and `withAnnotateState` because they are used by tactics (e.g., `rewrite`) to control
which goal is displayed in the info views. See issue #1403
-/
if info.stx.isOfKind nullKind || info.toElabInfo?.any (·.elaborator == `Lean.Elab.Tactic.evalWithAnnotateState) then
return results
unless (info matches .ofFieldInfo _ | .ofOptionInfo _ || info.toElabInfo?.isSome) && info.contains hoverPos includeStop do
return results
let r := info.range?.get!
let priority := (
-- prefer results directly *after* the hover position (only matters for `includeStop = true`; see #767)
if r.stop == hoverPos then 0 else 1,
-- relying on the info tree structure is _not_ sufficient for choosing the smallest surrounding node:
-- `⟨x⟩` expands to an application of a canonical syntax with the span of the anonymous constructor to `x`,
-- i.e. there are two info tree siblings whose spans are not disjoint and we should choose the smaller node
-- surrounding the cursor
Int.negOfNat (r.stop - r.start).byteIdx,
-- prefer results for constants over variables (which overlap at declaration names)
if info matches .ofTermInfo { expr := .fvar .., .. } then 0 else 1)
[(priority, {ctx, info, children})]) |>.getD []
-- sort results by lexicographical priority
let maxPrio? :=
let _ := @lexOrd
let _ := @leOfOrd.{0}
let _ := @maxOfLe
results.map (·.1) |>.maximum?
let res? := results.find? (·.1 == maxPrio?) |>.map (·.2)
if let some i := res? then
if let .ofTermInfo ti := i.info then
if ti.expr.isSyntheticSorry then
return none
return res?
def Info.type? (i : Info) : MetaM (Option Expr) :=
match i with
| Info.ofTermInfo ti => Meta.inferType ti.expr
| Info.ofFieldInfo fi => Meta.inferType fi.val
| _ => return none
def Info.docString? (i : Info) : MetaM (Option String) := do
let env ← getEnv
match i with
| Info.ofTermInfo ti =>
if let some n := ti.expr.constName? then
return ← findDocString? env n
| .ofFieldInfo fi => return ← findDocString? env fi.projName
| .ofOptionInfo oi =>
if let some doc ← findDocString? env oi.declName then
return doc
if let some decl := (← getOptionDecls).find? oi.optionName then
return decl.descr
return none
| _ => pure ()
if let some ei := i.toElabInfo? then
return ← findDocString? env ei.stx.getKind <||> findDocString? env ei.elaborator
return none
/-- Construct a hover popup, if any, from an info node in a context.-/
def Info.fmtHover? (ci : ContextInfo) (i : Info) : IO (Option FormatWithInfos) := do
ci.runMetaM i.lctx do
let mut fmts := #[]
let mut infos := ∅
let modFmt ← try
let (termFmt, modFmt) ← fmtTermAndModule?
if let some f := termFmt then
fmts := fmts.push f.fmt
infos := f.infos
pure modFmt
catch _ => pure none
if let some m ← i.docString? then
fmts := fmts.push m
if let some f := modFmt then
fmts := fmts.push f
if fmts.isEmpty then
return none
else
return some ⟨f!"\n***\n".joinSep fmts.toList, infos⟩
where
fmtModule? (decl : Name) : MetaM (Option Format) := do
let some mod ← findModuleOf? decl | return none
return some f!"*import {mod}*"
fmtTermAndModule? : MetaM (Option FormatWithInfos × Option Format) := do
match i with
| Info.ofTermInfo ti =>
let e ← instantiateMVars ti.expr
if e.isSort then
-- Types of sorts are funny to look at in widgets, but ultimately not very helpful
return (none, none)
let tp ← instantiateMVars (← Meta.inferType e)
let tpFmt ← Meta.ppExpr tp
if let .const c _ := e then
let eFmt ← PrettyPrinter.ppSignature c
return (some { eFmt with fmt := f!"```lean\n{eFmt.fmt}\n```" }, ← fmtModule? c)
let eFmt ← Meta.ppExpr e
-- Try not to show too scary internals
let showTerm := if let .fvar _ := e then
if let some ldecl := (← getLCtx).findFVar? e then
!ldecl.userName.hasMacroScopes
else false
else isAtomicFormat eFmt
let fmt := if showTerm then f!"{eFmt} : {tpFmt}" else tpFmt
return (some f!"```lean\n{fmt}\n```", none)
| Info.ofFieldInfo fi =>
let tp ← Meta.inferType fi.val
let tpFmt ← Meta.ppExpr tp
return (some f!"```lean\n{fi.fieldName} : {tpFmt}\n```", none)
| _ => return (none, none)
isAtomicFormat : Format → Bool
| Std.Format.text _ => true
| Std.Format.group f _ => isAtomicFormat f
| Std.Format.nest _ f => isAtomicFormat f
| Std.Format.tag _ f => isAtomicFormat f
| _ => false
structure GoalsAtResult where
ctxInfo : ContextInfo
tacticInfo : TacticInfo
useAfter : Bool
/-- Whether the tactic info is further indented than the hover position. -/
indented : Bool
-- for overlapping goals, only keep those of the highest reported priority
priority : Nat
/--
Try to retrieve `TacticInfo` for `hoverPos`.
We retrieve all `TacticInfo` nodes s.t. `hoverPos` is inside the node's range plus trailing whitespace.
We usually prefer the innermost such nodes so that for composite tactics such as `induction`, we show the nested proofs' states.
However, if `hoverPos` is after the tactic, we prefer nodes that are not indented relative to it, meaning that e.g. at `|` in
```lean
have := by
exact foo
|
```
we show the (final, see below) state of `have`, not `exact`.
Moreover, we instruct the LSP server to use the state after tactic execution if
- the hover position is after the info's start position *and*
- there is no nested tactic info after the hover position (tactic combinators should decide for themselves
where to show intermediate states by calling `withTacticInfoContext`) -/
partial def InfoTree.goalsAt? (text : FileMap) (t : InfoTree) (hoverPos : String.Pos) : List GoalsAtResult :=
let gs := t.collectNodesBottomUp fun ctx i cs gs => Id.run do
if let Info.ofTacticInfo ti := i then
if let (some pos, some tailPos) := (i.pos?, i.tailPos?) then
let trailSize := i.stx.getTrailingSize
-- show info at EOF even if strictly outside token + trail
let atEOF := tailPos.byteIdx + trailSize == text.source.endPos.byteIdx
-- include at least one trailing character (see also `priority` below)
if pos ≤ hoverPos ∧ (hoverPos.byteIdx < tailPos.byteIdx + max 1 trailSize || atEOF) then
-- overwrite bottom-up results according to "innermost" heuristics documented above
if gs.isEmpty || hoverPos ≥ tailPos && gs.all (·.indented) then
return [{
ctxInfo := ctx
tacticInfo := ti
useAfter := hoverPos > pos && !cs.any (hasNestedTactic pos tailPos)
-- consider every position unindented after an empty `by` to support "hanging" `by` uses
indented := (text.toPosition pos).column > (text.toPosition hoverPos).column && !isEmptyBy ti.stx
-- use goals just before cursor as fall-back only
-- thus for `(by foo)`, placing the cursor after `foo` shows its state as long
-- as there is no state on `)`
priority := if hoverPos.byteIdx == tailPos.byteIdx + trailSize then 0 else 1
}]
return gs
let maxPrio? := gs.map (·.priority) |>.maximum?
gs.filter (some ·.priority == maxPrio?)
where
hasNestedTactic (pos tailPos) : InfoTree → Bool
| InfoTree.node i@(Info.ofTacticInfo _) cs => Id.run do
if let `(by $_) := i.stx then
return false -- ignore term-nested proofs such as in `simp [show p by ...]`
if let (some pos', some tailPos') := (i.pos?, i.tailPos?) then
-- ignore preceding nested infos
-- ignore nested infos of the same tactic, e.g. from expansion
if tailPos' > hoverPos && (pos', tailPos') != (pos, tailPos) then
return true
cs.any (hasNestedTactic pos tailPos)
| InfoTree.node (Info.ofMacroExpansionInfo _) cs =>
cs.any (hasNestedTactic pos tailPos)
| _ => false
isEmptyBy (stx : Syntax) : Bool :=
-- there are multiple `by` kinds with the same structure
stx.getNumArgs == 2 && stx[0].isToken "by" && stx[1].getNumArgs == 1 && stx[1][0].isMissing
partial def InfoTree.termGoalAt? (t : InfoTree) (hoverPos : String.Pos) : Option InfoWithCtx :=
-- In the case `f a b`, where `f` is an identifier, the term goal at `f` should be the goal for the full application `f a b`.
hoverableInfoAt? t hoverPos (includeStop := true) (omitAppFns := true)
partial def InfoTree.hasSorry : InfoTree → IO Bool :=
go none
where go ci?
| .context ci t => go ci t
| .node i cs =>
if let (some ci, .ofTermInfo ti) := (ci?, i) then do
let expr ← ti.runMetaM ci (instantiateMVars ti.expr)
return expr.hasSorry
-- we assume that `cs` are subterms of `ti.expr` and
-- thus do not have to be checked as well
else
cs.anyM (go ci?)
| _ => return false
end Lean.Elab