lean4-htt/src/Lean/Server/FileWorker/SignatureHelp.lean
Markus Himmel dca8d6d188
refactor: discipline around arithmetic of String.Pos.Raw (#10713)
This PR enforces rules around arithmetic of `String.Pos.Raw`.

Specifically, it adopts the following conventions:

- Byte indices ("ordinals") in strings should be represented using
`String.Pos.Raw`
- Amounts of bytes ("cardinals") in strings should be represented using
`Nat`.

For example, `String.Slice.utf8ByteSize` now returns `Nat` instead of
`String.Pos.Raw`, and there is a new function `String.Slice.rawEndPos`.

Finally, the `HAdd` and `HSub` instances for `String.Pos.Raw` are
reorganized. This is a **breaking change**.

The `HAdd/HSub String.Pos.Raw String.Pos.Raw String.Pos.Raw` instances
have been removed. For the use case of tracking positions relative to
some other position, we instead provide `offsetBy` and `unoffsetBy`
functions. For the use case of advancing/unadvancing a position by an
arbitrary number of bytes, we instead provide `increaseBy` and
`decreaseBy` functions. For
offsetting/unoffsetting/advancing/unadvancing a position `p` by the size
of a string `s` (resp. character `c`), use `s + p`/`p - s`/`p + s`/`p -
s` (resp. `c + p`/`p - c`/`p + c`/`p - c`).
2025-10-09 07:47:45 +00:00

218 lines
10 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) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Marc Huisinga
-/
module
prelude
public import Lean.Server.InfoUtils
public import Lean.Data.Lsp
public import Init.Data.List.Sort.Basic
import Lean.PrettyPrinter.Delaborator
meta import Lean.Parser.Term
public section
namespace Lean.Server.FileWorker.SignatureHelp
open Lean
def determineSignatureHelp (tree : Elab.InfoTree) (appStx : Syntax)
: IO (Option Lsp.SignatureHelp) := do
let some (appCtx, .ofTermInfo appInfo) := tree.smallestInfo? fun
| .ofTermInfo ti =>
-- HACK: Use range of syntax to figure out corresponding `TermInfo`.
-- This is necessary because in order to accurately determine which `Syntax` to use,
-- we have to use the original command syntax before macro expansions,
-- whereas the `Syntax` in the `InfoTree` is always from some stage of elaboration.
ti.stx.getRangeWithTrailing? == appStx.getRangeWithTrailing?
| _ => false
| return none
let app := appInfo.expr
let some fmt ← appCtx.runMetaM appInfo.lctx do
let appType ← instantiateMVars <| ← Meta.inferType app
if ! appType.isForall then
return none
let (stx, _) ← PrettyPrinter.delabCore appType
(delab := PrettyPrinter.Delaborator.delabForallWithSignature)
return some <| ← PrettyPrinter.ppTerm ⟨stx⟩
| return none
return some {
signatures := #[{ label := toString fmt : Lsp.SignatureInformation }]
activeSignature? := some 0
-- We do not mark the active parameter at all, as this would require retaining parameter indices
-- through the delaborator.
-- However, since we display the signature help using the `TermInfo` of the application,
-- not the function itself, this is not a problem:
-- The parameters keeps reducing as one adds arguments to the function, and the active
-- parameter is then always the first explicit one.
-- This feels very intuitive, so we don't need to thread any additional information
-- through the delaborator for highlighting the active parameter.
activeParameter? := none
}
inductive CandidateKind where
/--
Cursor is in the position of the argument to a pipe, like `<|` or `$`. Low precedence.
Ensures that `fun <| otherFun <cursor>` yields the signature help of `otherFun`, not `fun`.
-/
| pipeArg
/--
Cursor is in the position of the trailing whitespace of some term. Medium precedence.
Ensures that `fun otherFun <cursor>` yields the signature help of `fun`, not `otherFun`.
-/
| termArg
/--
Cursor is in the position of the argument to a function that already has other arguments applied
to it. High precedence.
-/
| appArg
def CandidateKind.prio : CandidateKind → Nat
| .pipeArg => 0
| .termArg => 1
| .appArg => 2
structure Candidate where
kind : CandidateKind
appStx : Syntax
inductive SearchControl where
/-- In a syntax stack, keep searching upwards, continuing with the parent of the current term. -/
| continue
/-- Stop the search through a syntax stack. -/
| stop
private def lineCommentPosition? (s : String) : Option String.Pos.Raw := Id.run do
let mut it := s.mkIterator
while h : it.hasNext do
let pos := it.pos
let c₁ := it.curr' h
it := it.next' h
if c₁ == '-' then
if h' : it.hasNext then
let c₂ := it.curr' h'
it := it.next' h'
if c₂ == '-' then
return some pos
return none
private def isPositionInLineComment (text : FileMap) (pos : String.Pos.Raw) : Bool := Id.run do
let requestedLineNumber := text.toPosition pos |>.line
let lineStartPos := text.lineStart requestedLineNumber
let lineEndPos := text.lineStart (requestedLineNumber + 1)
let line := text.source.extract lineStartPos lineEndPos
let some lineCommentPos := lineCommentPosition? line
| return false
return pos >= lineCommentPos.offsetBy lineStartPos
open CandidateKind in
def findSignatureHelp? (text : FileMap) (ctx? : Option Lsp.SignatureHelpContext) (cmdStx : Syntax)
(tree : Elab.InfoTree) (requestedPos : String.Pos.Raw) : IO (Option Lsp.SignatureHelp) := do
-- HACK: Since comments are whitespace, the signature help can trigger on comments.
-- This is especially annoying on end-of-line comments, as the signature help will trigger on
-- every space in the comment.
-- This branch avoids this particular annoyance, but doesn't prevent the signature help from
-- triggering on other comment kinds.
if isPositionInLineComment text requestedPos then
return none
let stack? := cmdStx.findStack? fun stx => Id.run do
let some range := stx.getRangeWithTrailing? (canonicalOnly := true)
| return false
return range.contains requestedPos (includeStop := true)
let some stack := stack?
| return none
let stack := stack.toArray.map (·.1)
let mut candidates : Array Candidate := #[]
for h:i in *...stack.size do
let stx := stack[i]
let parent := stack[i+1]?.getD .missing
let (kind?, control) := determineCandidateKind stx parent
if let some kind := kind? then
candidates := candidates.push ⟨kind, stx⟩
if control matches .stop then
break
-- Uses a stable sort so that we prefer inner candidates over outer candidates.
candidates := candidates.toList.mergeSort (fun c1 c2 => c1.kind.prio >= c2.kind.prio) |>.toArray
-- Look through all candidates until we find a signature help.
-- This helps in cases where the priority puts terms without `TermInfo` or ones that are not
-- applications of a `forall` in front of ones that do.
-- This usually happens when `.termArg` candidates overshadow `.pipeArg` candidates,
-- but the `.termArg` candidates are not semantically valid left-hand sides of applications.
let hasHighPrioCandidate := candidates.any (·.kind.prio > CandidateKind.termArg.prio)
for candidate in candidates do
if candidate.kind matches .termArg && hasHighPrioCandidate then
-- If all high prio candidates agree that there is no function application here,
-- we trust them instead of falling back to `.termArg` candidates.
-- This ensures that no signature help is displayed for a function that is passed as
-- the last argument to another function.
return none
if let some signatureHelp ← determineSignatureHelp tree candidate.appStx then
return some signatureHelp
return none
where
determineCandidateKind (stx : Syntax) (parent : Syntax)
: Option CandidateKind × SearchControl := Id.run do
let c kind? : Option CandidateKind × SearchControl := (kind?, .continue)
let some tailPos := stx.getTailPos? (canonicalOnly := true)
| return (none, .continue)
-- If the cursor is not in the trailing range of the syntax, then we don't display a signature
-- help. This prevents two undesirable scenarios:
-- - Since for applications `f 0 0 0`, the `InfoTree` only contains `TermInfo` for
-- `f` and `f 0 0 0`, we can't display accurate signature help for the sub-applications
-- `f 0` or `f 0 0`. Hence, we only display the signature help after `f` and `f 0 0 0`,
-- i.e. in the trailing range of the syntax of a candidate.
-- - When the search through the syntax stack passes through a node with more than one child
-- that is not an application, terminating the search if the cursor is on the interior of the
-- syntax ensures that we do not display signature help for functions way outside of the
-- current term that is being edited.
-- We still want to display it for such complex terms if we are in the trailing range of the
-- term since the complex term might produce a function for which we want to display a
-- signature help.
-- If we are ever on the interior of a term, then we will also be on the interior of terms
-- further up in the syntax stack, as these subsume the inner term, and so we terminate
-- the search early in this case.
if requestedPos < tailPos then
return (none, .stop)
let isManualTrigger := ctx?.any (·.triggerKind matches .invoked)
let isRetrigger := ctx?.any (·.isRetrigger)
let isCursorAfterTailPosLine :=
(text.toPosition requestedPos).line != (text.toPosition tailPos).line
-- Application arguments are allowed anywhere in the trailing whitespace of a function,
-- e.g. on successive lines, but displaying the signature help in all of those lines
-- can be annoying (e.g. when `#check`ing a function and typing in the lines after it).
-- Hence, we only display the signature help automatically when the cursor is on the same line
-- as the tail position of the syntax, but allow users to display it by manually triggering
-- the signature help (`Ctrl+Shift+Space` in VS Code). We also display it in successive lines
-- if the user never closed it in the meantime, i.e. when the signature help was simply
-- retriggered.
if ! isManualTrigger && ! isRetrigger && isCursorAfterTailPosLine then
return (none, .continue)
if stx matches .ident .. then
match parent with
-- Do not yield a candidate `f` for `_ |>.f <cursor>`, `_.f <cursor>` or `.f <cursor>`.
-- Since `f` is an `identArg` candidate, has a `TermInfo` of its own and is a subterm of
-- these dot notations, we need to avoid picking its `TermInfo` by accident.
| `($_ |>.$_:ident $_*) => return c none
| `($_.$_:ident) => return c none
| `(.$_:ident) => return c none
| _ => return c termArg
let .node (kind := kind) (args := args) .. := stx
| return c none
-- `nullKind` is usually used for grouping together arguments, so we just skip it until
-- we have more tangible nodes at hand.
if kind == nullKind then
return c none
if kind == ``Parser.Term.app then
return c appArg
match stx with
| `($_ <| $_) => return c pipeArg
| `($_ $ $_) => return c pipeArg
| `($_ |>.$_:ident $_*) => return c pipeArg
| `(.$_:ident) => return c termArg
| `($_.$_:ident) => return c termArg
| _ =>
if args.size <= 1 then
return c none
return c termArg