lean4-htt/src/Lean/Exception.lean
Marc Huisinga 336b68ec20
feat: 'unknown identifier' code actions (#7665)
This PR adds support for code actions that resolve 'unknown identifier'
errors by either importing the missing declaration or by changing the
identifier to one from the environment.

<details>
<summary>Demo (Click to open)</summary>


![Demo](https://github.com/user-attachments/assets/ba575860-b76d-4213-8cd7-a5525cd60287)
</details>

Specifically, the following kinds of code actions are added by this PR,
all of which are triggered on 'unknown identifier' errors:
- A code action to import the module containing the identifier at the
text cursor position.
- A code action to change the identifier at the text cursor position to
one from the environment.
- A source action to import the modules for all unambiguous identifiers
in the file.

### Details
When clicking on an identifier with an 'unknown identifier' diagnostic,
after a debounce delay of 1000ms, the language server looks up the
(potentially partial) identifier at the position of the cursor in the
global reference data structure by fuzzy-matching against all
identifiers and collects the 10 closest matching entries. This search
accounts for open namespaces at the position of the cursor, including
the namespace of the type / expected type when using dot notation. The
10 closest matching entries are then offered to the user as code
actions:
- If the suggested identifier is not contained in the environment, a
code action that imports the module that the identifier is contained in
and changes the identifier to the suggested one is offered. The
suggestion is inserted in a "minimal" manner, i.e. by accounting for
open namespaces.
- If the suggested identifier is contained in the environment, a code
action that only changes the identifier to the suggested one is offered.
- If the suggested identifier is not contained in the environment and
the suggested identifier is a perfectly unambiguous match, a source
action to import all unambiguous in the file is offered.

The source action to import all unambiguous identifiers can also always
be triggered by right-clicking in the document and selecting the 'Source
Action...' entry.

At the moment, for large projects, the search for closely matching
identifiers in the global reference data structure is still a bit slow.
I hope to optimize it next quarter.

### Implementation notes
- Since the global reference data structure is in the watchdog process,
whereas the elaboration information is in the file worker process, this
PR implements support for file worker -> watchdog requests, including a
new `$/lean/queryModule` request that can be used by the file worker to
request global identifier information.
- To identify 'unknown identifier' errors, several 'unknown identifier'
errors in the elaborator are tagged with a new tag.
- The debounce delay of 1000ms is necessary because VS Code will
re-request code actions while editing an unknown identifier and also
while hovering over the identifier.
- We also implement cancellation for these 'unknown identifier' code
actions. Once the file worker responds to the request as having been
cancelled, the watchdog cancels its computation of all corresponding
file worker -> watchdog requests, too.
- Aliases (i.e. `export`) are currently not accounted for. I've found
that we currently don't handle them correctly in auto-completion, too,
so we will likely add support for this later when fixing the
corresponding auto-completion issue.
- The new code actions added by this request support incrementality.
2025-04-02 09:43:40 +00:00

209 lines
7.8 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) 2020 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Lean.Message
import Lean.InternalExceptionId
import Lean.Data.Options
import Lean.Util.MonadCache
namespace Lean
/-- Exception type used in most Lean monads -/
inductive Exception where
/-- Error messages that are displayed to users. `ref` is used to provide position information. -/
| error (ref : Syntax) (msg : MessageData)
/--
Internal exceptions that are not meant to be seen by users.
Examples: "postpone elaboration", "stuck at universe constraint", etc.
-/
| internal (id : InternalExceptionId) (extra : KVMap := {})
/-- Convert exception into a structured message. -/
def Exception.toMessageData : Exception → MessageData
| .error _ msg => msg
| .internal id _ => id.toString
def Exception.hasSyntheticSorry : Exception → Bool
| Exception.error _ msg => msg.hasSyntheticSorry
| _ => false
/--
Return syntax object providing position information for the exception.
Recall that internal exceptions do not have position information.
-/
def Exception.getRef : Exception → Syntax
| .error ref _ => ref
| .internal _ _ => Syntax.missing
instance : Inhabited Exception := ⟨Exception.error default default⟩
/-- Similar to `AddMessageContext`, but for error messages.
The default instance just uses `AddMessageContext`.
In error messages, we may want to provide additional information (e.g., macro expansion stack),
and refine the `(ref : Syntax)`. -/
class AddErrorMessageContext (m : Type → Type) where
add : Syntax → MessageData → m (Syntax × MessageData)
instance (m : Type → Type) [AddMessageContext m] [Monad m] : AddErrorMessageContext m where
add ref msg := do
let msg ← addMessageContext msg
pure (ref, msg)
class abbrev MonadError (m : Type → Type) :=
MonadExceptOf Exception m
MonadRef m
AddErrorMessageContext m
section Methods
/--
Throw an error exception using the given message data.
The result of `getRef` is used as position information.
Recall that `getRef` returns the current "reference" syntax.
-/
protected def throwError [Monad m] [MonadError m] (msg : MessageData) : m α := do
let ref ← getRef
let (ref, msg) ← AddErrorMessageContext.add ref msg
throw <| Exception.error ref msg
/--
Tag used for `unknown identifier` messages.
This tag is used by the 'import unknown identifier' code action to detect messages that should
prompt the code action.
-/
def unknownIdentifierMessageTag : Name := `unknownIdentifier
/--
Creates a `MessageData` that is tagged with `unknownIdentifierMessageTag`.
This tag is used by the 'import unknown identifier' code action to detect messages that should
prompt the code action.
-/
def mkUnknownIdentifierMessage (msg : MessageData) : MessageData :=
MessageData.tagged unknownIdentifierMessageTag msg
/--
Throw an unknown identifier error message that is tagged with `unknownIdentifierMessageTag`.
See also `mkUnknownIdentifierMessage`.
-/
def throwUnknownIdentifier [Monad m] [MonadError m] (msg : MessageData) : m α :=
Lean.throwError <| mkUnknownIdentifierMessage msg
/-- Throw an unknown constant error message. -/
def throwUnknownConstant [Monad m] [MonadError m] (constName : Name) : m α :=
throwUnknownIdentifier m!"unknown constant '{.ofConstName constName}'"
/-- Throw an error exception using the given message data and reference syntax. -/
protected def throwErrorAt [Monad m] [MonadError m] (ref : Syntax) (msg : MessageData) : m α := do
withRef ref <| Lean.throwError msg
/--
Convert an `Except` into a `m` monadic action, where `m` is any monad that
implements `MonadError`.
-/
def ofExcept [Monad m] [MonadError m] [ToMessageData ε] (x : Except ε α) : m α :=
match x with
| .ok a => return a
| .error e => Lean.throwError <| toMessageData e
builtin_initialize interruptExceptionId : InternalExceptionId ← registerInternalExceptionId `interrupt
/--
Throws an internal interrupt exception that skips standard `catch` clauses and should be caught only
at the top level of elaboration.
-/
def throwInterruptException [Monad m] [MonadError m] [MonadOptions m] : m α :=
throw <| .internal interruptExceptionId
/-- Returns `true` if the exception is an interrupt generated by `checkInterrupted`. -/
def Exception.isInterrupt : Exception → Bool
| Exception.internal id _ => id == interruptExceptionId
| _ => false
/--
Throw an error exception for the given kernel exception.
-/
def throwKernelException [Monad m] [MonadError m] [MonadOptions m] (ex : Kernel.Exception) : m α := do
if ex matches .interrupted then
throwInterruptException
Lean.throwError <| ex.toMessageData (← getOptions)
/-- Lift from `Except KernelException` to `m` when `m` can throw kernel exceptions. -/
def ofExceptKernelException [Monad m] [MonadError m] [MonadOptions m] (x : Except Kernel.Exception α) : m α :=
match x with
| .ok a => return a
| .error e => throwKernelException e
end Methods
class MonadRecDepth (m : Type → Type) where
withRecDepth {α} : Nat → m α → m α
getRecDepth : m Nat
getMaxRecDepth : m Nat
instance [MonadRecDepth m] : MonadRecDepth (ReaderT ρ m) where
withRecDepth d x := fun ctx => MonadRecDepth.withRecDepth d (x ctx)
getRecDepth := fun _ => MonadRecDepth.getRecDepth
getMaxRecDepth := fun _ => MonadRecDepth.getMaxRecDepth
instance [Monad m] [MonadRecDepth m] : MonadRecDepth (StateRefT' ω σ m) :=
inferInstanceAs (MonadRecDepth (ReaderT _ _))
instance [BEq α] [Hashable α] [Monad m] [STWorld ω m] [MonadRecDepth m] : MonadRecDepth (MonadCacheT α β m) :=
inferInstanceAs (MonadRecDepth (StateRefT' _ _ _))
/--
Throw a "maximum recursion depth has been reached" exception using the given reference syntax.
-/
def throwMaxRecDepthAt [MonadError m] (ref : Syntax) : m α :=
throw <| .error ref (.tagged `runtime.maxRecDepth <| MessageData.ofFormat (Std.Format.text maxRecDepthErrorMessage))
/--
Return true if `ex` was generated by `throwMaxRecDepthAt`.
This function is a bit hackish. The max rec depth exception should probably be an internal exception,
but it is also produced by `MacroM` which implemented in the prelude, and internal exceptions have not
been defined yet.
-/
def Exception.isMaxRecDepth (ex : Exception) : Bool :=
ex matches error _ (.tagged `runtime.maxRecDepth _)
/--
Increment the current recursion depth and then execute `x`.
Throw an exception if maximum recursion depth has been reached.
We use this combinator to prevent stack overflows.
-/
@[inline] def withIncRecDepth [Monad m] [MonadError m] [MonadRecDepth m] (x : m α) : m α := do
let curr ← MonadRecDepth.getRecDepth
let max ← MonadRecDepth.getMaxRecDepth
if curr == max then
throwMaxRecDepthAt (← getRef)
else
MonadRecDepth.withRecDepth (curr+1) x
/--
Macro for throwing error exceptions. The argument can be an interpolated string.
It is a convenient way of building `MessageData` objects.
The result of `getRef` is used as position information.
Recall that `getRef` returns the current "reference" syntax.
-/
syntax "throwError " (interpolatedStr(term) <|> term) : term
/--
Macro for throwing error exceptions. The argument can be an interpolated string.
It is a convenient way of building `MessageData` objects.
The first argument must be a `Syntax` that provides position information for
the error message.
`throwErrorAt ref msg` is equivalent to `withRef ref <| throwError msg`
-/
syntax "throwErrorAt " term:max ppSpace (interpolatedStr(term) <|> term) : term
macro_rules
| `(throwError $msg:interpolatedStr) => `(Lean.throwError (m! $msg))
| `(throwError $msg:term) => `(Lean.throwError $msg)
macro_rules
| `(throwErrorAt $ref $msg:interpolatedStr) => `(Lean.throwErrorAt $ref (m! $msg))
| `(throwErrorAt $ref $msg:term) => `(Lean.throwErrorAt $ref $msg)
end Lean