lean4-htt/src/Lean/Server/InfoUtils.lean
2021-07-05 19:42:01 +02:00

251 lines
8.9 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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.DocString
import Lean.Elab.InfoTree
import Lean.Util.Sorry
protected structure String.Range where
start : String.Pos
stop : String.Pos
deriving Inhabited, Repr
def String.Range.contains (r : String.Range) (pos : String.Pos) : Bool :=
r.start <= pos && pos < r.stop
def Lean.Syntax.getRange? (stx : Syntax) (originalOnly := false) : Option String.Range :=
match stx.getPos? originalOnly, stx.getTailPos? originalOnly with
| some start, some stop => some { start, stop }
| _, _ => none
namespace Lean.Elab
/--
For every branch, find the deepest node in that branch matching `p`
with a surrounding context (the innermost one) and return all of them. -/
partial def InfoTree.deepestNodes (p : ContextInfo → Info → Std.PersistentArray InfoTree → Option α) : InfoTree → List α :=
go none
where go ctx?
| context ctx t => go ctx t
| n@(node i cs) =>
let ccs := cs.toList.map (go <| i.updateContext? ctx?)
let cs' := ccs.join
if !cs'.isEmpty then cs'
else match ctx? with
| some ctx => match p ctx i cs with
| some a => [a]
| _ => []
| _ => []
| _ => []
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
| ofFieldInfo i => i.stx
| ofCompletionInfo 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? (originalOnly := true)
def Info.tailPos? (i : Info) : Option String.Pos :=
i.stx.getTailPos? (originalOnly := true)
def Info.range? (i : Info) : Option String.Range :=
i.stx.getRange? (originalOnly := true)
def Info.contains (i : Info) (pos : String.Pos) : Bool :=
i.range?.any (·.contains pos)
def Info.size? (i : Info) : Option Nat := OptionM.run 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 Nat := OptionM.run do
let tailPos ← i.tailPos?
guard (tailPos ≤ hoverPos)
return hoverPos - tailPos
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) : Option (ContextInfo × Info) :=
t.smallestInfo? fun i => do
if let Info.ofTermInfo ti := i then
if ti.expr.isSyntheticSorry then
return false
if i matches Info.ofFieldInfo _ || i.toElabInfo?.isSome then
return i.contains hoverPos
return false
/-- Construct a hover popup, if any, from an info node in a context.-/
def Info.fmtHover? (ci : ContextInfo) (i : Info) : IO (Option Format) := do
ci.runMetaM i.lctx do
let mut fmts := #[]
try
if let some f ← fmtTerm? then
fmts := fmts.push f
catch _ => pure ()
if let some f ← fmtDoc? then
fmts := fmts.push f
if fmts.isEmpty then
none
else
f!"\n***\n".joinSep fmts.toList
where
fmtTerm? := do
match i with
| Info.ofTermInfo ti =>
let tp ← Meta.inferType ti.expr
let eFmt ← Meta.ppExpr ti.expr
let tpFmt ← Meta.ppExpr tp
-- try not to show too scary internals
let fmt := if isAtomicFormat eFmt then f!"{eFmt} : {tpFmt}" else tpFmt
return some f!"```lean
{fmt}
```"
| Info.ofFieldInfo fi =>
let tp ← Meta.inferType fi.val
let tpFmt ← Meta.ppExpr tp
return some f!"```lean
{fi.fieldName} : {tpFmt}
```"
| _ => return none
fmtDoc? := do
if let Info.ofTermInfo ti := i then
if let some n := ti.expr.constName? then
return ← findDocString? n
if let Info.ofFieldInfo fi := i then
return ← findDocString? fi.projName
if let some ei := i.toElabInfo? then
return ← findDocString? ei.elaborator <||> findDocString? ei.stx.getKind
return none
isAtomicFormat : Format → Bool
| Std.Format.text _ => true
| Std.Format.group f _ => isAtomicFormat f
| Std.Format.nest _ f => isAtomicFormat f
| _ => false
structure GoalsAtResult where
ctxInfo : ContextInfo
tacticInfo : TacticInfo
useAfter : Bool
/-
Try to retrieve `TacticInfo` for `hoverPos`.
We retrieve the `TacticInfo` `info`, if there is a node of the form `node (ofTacticInfo info) children` s.t.
- `hoverPos` is sufficiently inside `info`'s range (see code), and
- None of the `children` satisfy the condition above. That is, for composite tactics such as
`induction`, we always give preference for information stored in nested (children) tactics.
Moreover, we instruct the LSP server to use the state after the tactic execution if the hover is inside the info *and*
there is no nested tactic info (i.e. it is a leaf tactic; tactic combinators should decide for themselves
where to show intermediate/final states)
-/
partial def InfoTree.goalsAt? (text : FileMap) (t : InfoTree) (hoverPos : String.Pos) : List GoalsAtResult := do
t.deepestNodes fun
| ctx, i@(Info.ofTacticInfo ti), cs => OptionM.run do
let (some pos, some tailPos) ← pure (i.pos?, i.tailPos?)
| failure
let trailSize := i.stx.getTrailingSize
-- show info at EOF even if strictly outside token + trail
let atEOF := tailPos == text.source.bsize
guard <| pos ≤ hoverPos ∧ (hoverPos < tailPos + trailSize || atEOF)
return { ctxInfo := ctx, tacticInfo := ti, useAfter :=
hoverPos > pos && (hoverPos >= tailPos || !cs.any (hasNestedTactic pos tailPos)) }
| _, _, _ => none
where
hasNestedTactic (pos tailPos) : InfoTree → Bool
| InfoTree.node i@(Info.ofTacticInfo _) cs => do
if let `(by $t) := 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 nested infos of the same tactic, e.g. from expansion
if (pos', tailPos') != (pos, tailPos) then
return true
cs.any (hasNestedTactic pos tailPos)
| InfoTree.node (Info.ofMacroExpansionInfo _) cs =>
cs.any (hasNestedTactic pos tailPos)
| _ => false
/--
Find info nodes that should be used for the term goal feature.
The main complication concerns applications
like `f a b` where `f` is an identifier.
In this case, the term goal at `f`
should be the goal for the full application `f a b`.
Therefore we first gather the position of
these head function symbols such as `f`,
and later ignore identifiers at these positions.
-/
partial def InfoTree.termGoalAt? (t : InfoTree) (hoverPos : String.Pos) : Option (ContextInfo × Info) :=
let headFns : Std.HashSet String.Pos := t.foldInfo (init := {}) fun ctx i headFns => do
if let some pos := getHeadFnPos? i.stx then
headFns.insert pos
else
headFns
t.smallestInfo? fun i => do
if i.contains hoverPos then
if let Info.ofTermInfo ti := i then
return !ti.stx.isIdent || !headFns.contains i.pos?.get!
false
where
/- Returns the position of the head function symbol, if it is an identifier. -/
getHeadFnPos? (s : Syntax) (foundArgs := false) : Option String.Pos :=
match s with
| `(($s)) => getHeadFnPos? s foundArgs
| `($f $as*) => getHeadFnPos? f (foundArgs := foundArgs || !as.isEmpty)
| stx => if foundArgs && stx.isIdent then stx.getPos? else none
end Lean.Elab