feat: completion fallback (#5299)
When the elaborator doesn't provide us with any `CompletionInfo`, we currently provide no completions whatsoever. But in many cases, we can still provide some helpful identifier completions without elaborator information. This PR adds a fallback mode for this situation. There is more potential here, but this should be a good start. In principle, this issue alleviates #5172 (since we now provide completions in these contexts). I'll leave it up to an elaboration maintainer whether we also want to ensure that the completion infos are provided correctly in these cases.
This commit is contained in:
parent
273b7540b2
commit
b34379554d
6 changed files with 187 additions and 18 deletions
|
|
@ -666,6 +666,50 @@ private def tacticCompletion (params : CompletionParams) (ctx : ContextInfo) : I
|
|||
}, 1)
|
||||
return some { items := sortCompletionItems items, isIncomplete := true }
|
||||
|
||||
/--
|
||||
If there are `Info`s that contain `hoverPos` and have a nonempty `LocalContext`,
|
||||
yields the closest one of those `Info`s.
|
||||
Otherwise, yields the closest `Info` that contains `hoverPos` and has an empty `LocalContext`.
|
||||
-/
|
||||
private def findClosestInfoWithLocalContextAt?
|
||||
(hoverPos : String.Pos)
|
||||
(infoTree : InfoTree)
|
||||
: Option (ContextInfo × Info) :=
|
||||
infoTree.visitM (m := Id) (postNode := choose) |>.join
|
||||
where
|
||||
choose
|
||||
(ctx : ContextInfo)
|
||||
(info : Info)
|
||||
(_ : PersistentArray InfoTree)
|
||||
(childValues : List (Option (Option (ContextInfo × Info))))
|
||||
: Option (ContextInfo × Info) :=
|
||||
let bestChildValue := childValues.map (·.join) |>.foldl (init := none) fun v best =>
|
||||
if isBetter v best then
|
||||
v
|
||||
else
|
||||
best
|
||||
if info.occursInOrOnBoundary hoverPos && isBetter (ctx, info) bestChildValue then
|
||||
(ctx, info)
|
||||
else
|
||||
bestChildValue
|
||||
|
||||
isBetter (a b : Option (ContextInfo × Info)) : Bool :=
|
||||
match a, b with
|
||||
| none, none => false
|
||||
| some _, none => true
|
||||
| none, some _ => false
|
||||
| some (_, ia), some (_, ib) =>
|
||||
if !ia.lctx.isEmpty && ib.lctx.isEmpty then
|
||||
true
|
||||
else if ia.lctx.isEmpty && !ib.lctx.isEmpty then
|
||||
false
|
||||
else if ia.isSmaller ib then
|
||||
true
|
||||
else if ib.isSmaller ia then
|
||||
false
|
||||
else
|
||||
false
|
||||
|
||||
private def findCompletionInfoAt?
|
||||
(fileMap : FileMap)
|
||||
(hoverPos : String.Pos)
|
||||
|
|
@ -675,9 +719,32 @@ private def findCompletionInfoAt?
|
|||
match infoTree.foldInfo (init := none) (choose hoverLine) with
|
||||
| some (hoverInfo, ctx, Info.ofCompletionInfo info) =>
|
||||
some (hoverInfo, ctx, info)
|
||||
| _ =>
|
||||
-- TODO try to extract id from `fileMap` and some `ContextInfo` from `InfoTree`
|
||||
none
|
||||
| _ => do
|
||||
-- No completion info => Attempt providing identifier completions
|
||||
let some (ctx, info) := findClosestInfoWithLocalContextAt? hoverPos infoTree
|
||||
| none
|
||||
let some stack := info.stx.findStack? (·.getRange?.any (·.contains hoverPos (includeStop := true)))
|
||||
| none
|
||||
let stack := stack.dropWhile fun (stx, _) => !(stx matches `($_:ident) || stx matches `($_:ident.))
|
||||
let some (stx, _) := stack.head?
|
||||
| none
|
||||
let isDotIdCompletion := stack.any fun (stx, _) => stx matches `(.$_:ident)
|
||||
if isDotIdCompletion then
|
||||
-- An identifier completion is never useful in a dotId completion context.
|
||||
none
|
||||
let some (id, danglingDot) :=
|
||||
match stx with
|
||||
| `($id:ident) => some (id.getId, false)
|
||||
| `($id:ident.) => some (id.getId, true)
|
||||
| _ => none
|
||||
| none
|
||||
let tailPos := stx.getTailPos?.get!
|
||||
let hoverInfo :=
|
||||
if hoverPos < tailPos then
|
||||
HoverInfo.inside (tailPos - hoverPos).byteIdx
|
||||
else
|
||||
HoverInfo.after
|
||||
some (hoverInfo, ctx, .id stx id danglingDot info.lctx none)
|
||||
where
|
||||
choose
|
||||
(hoverLine : Nat)
|
||||
|
|
|
|||
|
|
@ -141,10 +141,12 @@ def Info.stx : Info → Syntax
|
|||
| ofOmissionInfo i => i.stx
|
||||
|
||||
def Info.lctx : Info → LocalContext
|
||||
| Info.ofTermInfo i => i.lctx
|
||||
| Info.ofFieldInfo i => i.lctx
|
||||
| Info.ofOmissionInfo i => i.lctx
|
||||
| _ => LocalContext.empty
|
||||
| .ofTermInfo i => i.lctx
|
||||
| .ofFieldInfo i => i.lctx
|
||||
| .ofOmissionInfo i => i.lctx
|
||||
| .ofMacroExpansionInfo i => i.lctx
|
||||
| .ofCompletionInfo i => i.lctx
|
||||
| _ => LocalContext.empty
|
||||
|
||||
def Info.pos? (i : Info) : Option String.Pos :=
|
||||
i.stx.getPos? (canonicalOnly := true)
|
||||
|
|
|
|||
31
tests/lean/interactive/completionFallback.lean
Normal file
31
tests/lean/interactive/completionFallback.lean
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- When the elaborator doesn't provide `CompletionInfo`, try to provide identifier completions.
|
||||
-- As of when this test case was written, the elaborator did not provide `CompletionInfo` in these cases.
|
||||
|
||||
-- https://github.com/leanprover/lean4/issues/5172
|
||||
|
||||
inductive Direction where
|
||||
| up
|
||||
| right
|
||||
| down
|
||||
| left
|
||||
deriving Repr
|
||||
|
||||
def angle (d: Direction) :=
|
||||
match d with
|
||||
| Direction. => 90
|
||||
--^ textDocument/completion
|
||||
| Direction.right => 0
|
||||
| Direction.down => 270
|
||||
| Direction.left => 180
|
||||
|
||||
-- Ensure that test is stable when changes to the `And` namespace are made.
|
||||
structure CustomAnd (a b : Prop) : Prop where
|
||||
ha : a
|
||||
hb : b
|
||||
|
||||
example : p ∨ (q ∧ r) → CustomAnd (p ∨ q) (p ∨ r) := by
|
||||
intro h
|
||||
cases h with
|
||||
| inl hp => apply CustomAnd. (Or.intro_left q hp) (Or.intro_left r hp)
|
||||
--^ textDocument/completion
|
||||
| inr hqr => apply CustomAnd.mk (Or.intro_right p hqr.left) (Or.intro_right p hqr.right)
|
||||
80
tests/lean/interactive/completionFallback.lean.expected.out
Normal file
80
tests/lean/interactive/completionFallback.lean.expected.out
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}}
|
||||
{"items":
|
||||
[{"sortText": "0",
|
||||
"label": "down",
|
||||
"kind": 4,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.down"}}}},
|
||||
{"sortText": "1",
|
||||
"label": "left",
|
||||
"kind": 4,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.left"}}}},
|
||||
{"sortText": "2",
|
||||
"label": "noConfusionType",
|
||||
"kind": 3,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.noConfusionType"}}}},
|
||||
{"sortText": "3",
|
||||
"label": "right",
|
||||
"kind": 4,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.right"}}}},
|
||||
{"sortText": "4",
|
||||
"label": "toCtorIdx",
|
||||
"kind": 3,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.toCtorIdx"}}}},
|
||||
{"sortText": "5",
|
||||
"label": "up",
|
||||
"kind": 4,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 14, "character": 14}},
|
||||
"id": {"const": {"declName": "Direction.up"}}}}],
|
||||
"isIncomplete": true}
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 28, "character": 30}}
|
||||
{"items":
|
||||
[{"sortText": "0",
|
||||
"label": "ha",
|
||||
"kind": 5,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 28, "character": 30}},
|
||||
"id": {"const": {"declName": "CustomAnd.ha"}}}},
|
||||
{"sortText": "1",
|
||||
"label": "hb",
|
||||
"kind": 5,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 28, "character": 30}},
|
||||
"id": {"const": {"declName": "CustomAnd.hb"}}}},
|
||||
{"sortText": "2",
|
||||
"label": "mk",
|
||||
"kind": 4,
|
||||
"data":
|
||||
{"params":
|
||||
{"textDocument": {"uri": "file:///completionFallback.lean"},
|
||||
"position": {"line": 28, "character": 30}},
|
||||
"id": {"const": {"declName": "CustomAnd.mk"}}}}],
|
||||
"isIncomplete": true}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import Lean
|
||||
|
||||
open Lean Elab
|
||||
|
||||
def f (s : DefView) : DefView := {
|
||||
m
|
||||
--^ textDocument/completion
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{"textDocument": {"uri": "file:///fieldCompletion.lean"},
|
||||
"position": {"line": 5, "character": 3}}
|
||||
{"items": [], "isIncomplete": true}
|
||||
Loading…
Add table
Reference in a new issue