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>  </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.
209 lines
7.8 KiB
Text
209 lines
7.8 KiB
Text
/-
|
||
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
|