lean4-htt/src/Lean/Server/CodeActions/UnknownIdentifier.lean
Jason Yuen facc356a0a
chore: fix spelling errors (#10042)
Typos were found with
```
pip install codespell --upgrade
codespell --summary --ignore-words-list enew,forin,fro,happend,hge,ihs,iterm,spred --skip stage0 --check-filenames
codespell --summary --ignore-words-list enew,forin,fro,happend,hge,ihs,iterm,spred --skip stage0 --check-filenames --regex '[A-Z][a-z]*'
codespell --summary --ignore-words-list enew,forin,fro,happend,hge,ihs,iterm,spred --skip stage0 --check-filenames --regex "\b[a-z']*"
```
2025-08-22 07:23:12 +00:00

331 lines
11 KiB
Text

/-
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.FileWorker.Utils
public import Lean.Data.Lsp.Internal
public import Lean.Server.Requests
public import Lean.Server.Completion.CompletionInfoSelection
public import Lean.Server.CodeActions.Basic
public import Lean.Server.Completion.CompletionUtils
public section
namespace Lean.Server.FileWorker
open Lean.Lsp
open Lean.Server.Completion
structure UnknownIdentifierInfo where
paramsRange : String.Range
diagRange : String.Range
def waitUnknownIdentifierRanges (doc : EditableDocument) (requestedRange : String.Range)
: BaseIO (Array String.Range) := do
let text := doc.meta.text
let some parsedSnap := RequestM.findCmdParsedSnap doc requestedRange.start |>.get
| return #[]
let msgLog := Language.toSnapshotTree parsedSnap.elabSnap |>.collectMessagesInRange requestedRange |>.get
let mut ranges := #[]
for msg in msgLog.unreported do
if ! msg.data.hasTag (· == unknownIdentifierMessageTag) then
continue
let msgRange : String.Range := ⟨text.ofPosition msg.pos, text.ofPosition <| msg.endPos.getD msg.pos⟩
if ! msgRange.overlaps requestedRange
(includeFirstStop := true) (includeSecondStop := true) then
continue
ranges := ranges.push msgRange
return ranges
def waitAllUnknownIdentifierRanges (doc : EditableDocument)
: BaseIO (Array String.Range) := do
let text := doc.meta.text
let msgLog : MessageLog := Language.toSnapshotTree doc.initSnap
|>.getAll.map (·.diagnostics.msgLog)
|>.foldl (· ++ ·) {}
let mut ranges := #[]
for msg in msgLog.unreported do
if ! msg.data.hasTag (· == unknownIdentifierMessageTag) then
continue
let msgRange : String.Range := ⟨text.ofPosition msg.pos, text.ofPosition <| msg.endPos.getD msg.pos⟩
ranges := ranges.push msgRange
return ranges
structure Insertion where
fullName : Name
edit : TextEdit
structure Query extends LeanModuleQuery where
env : Environment
determineInsertion : Name → Insertion
partial def collectOpenNamespaces (currentNamespace : Name) (openDecls : List OpenDecl)
: Array OpenNamespace := Id.run do
let mut openNamespaces : Array OpenNamespace := #[]
let mut currentNamespace := currentNamespace
while ! currentNamespace.isAnonymous do
openNamespaces := openNamespaces.push <| .allExcept currentNamespace #[]
currentNamespace := currentNamespace.getPrefix
let openDeclNamespaces := openDecls.map fun
| .simple ns except => .allExcept ns except.toArray
| .explicit id declName => .renamed declName id
openNamespaces := openNamespaces ++ openDeclNamespaces.toArray
return openNamespaces
def computeIdQuery?
(doc : EditableDocument)
(ctx : Elab.ContextInfo)
(stx : Syntax)
(id : Name)
: Option Query := do
let some pos := stx.getPos? (canonicalOnly := true)
| none
let some tailPos := stx.getTailPos? (canonicalOnly := true)
| none
return {
identifier := id.toString
openNamespaces := collectOpenNamespaces ctx.currNamespace ctx.openDecls
env := ctx.env
determineInsertion decl :=
let minimizedId := minimizeGlobalIdentifierInContext ctx.currNamespace ctx.openDecls decl
{
fullName := minimizedId
edit := {
range := doc.meta.text.utf8RangeToLspRange ⟨pos, tailPos⟩
newText := minimizedId.toString
}
}
}
def computeDotQuery?
(doc : EditableDocument)
(ctx : Elab.ContextInfo)
(ti : Elab.TermInfo)
: IO (Option Query) := do
let text := doc.meta.text
let some pos := ti.stx.getPos? (canonicalOnly := true)
| return none
let some tailPos := ti.stx.getTailPos? (canonicalOnly := true)
| return none
let typeNames? : Option (Array Name) ← ctx.runMetaM ti.lctx do
try
return some <| ← getDotCompletionTypeNames (← Lean.instantiateMVars (← Lean.Meta.inferType ti.expr))
catch _ =>
return none
let some typeNames := typeNames?
| return none
if typeNames.isEmpty then
return none
return some {
identifier := text.source.extract pos tailPos
openNamespaces := typeNames.map (.allExcept · #[])
env := ctx.env
determineInsertion decl :=
{
fullName := decl
edit := {
range := text.utf8RangeToLspRange ⟨pos, tailPos⟩
newText := decl.getString!
}
}
}
def computeDotIdQuery?
(doc : EditableDocument)
(ctx : Elab.ContextInfo)
(stx : Syntax)
(id : Name)
(lctx : LocalContext)
(expectedType? : Option Expr)
: IO (Option Query) := do
let some pos := stx.getPos? (canonicalOnly := true)
| return none
let some tailPos := stx.getTailPos? (canonicalOnly := true)
| return none
let some expectedType := expectedType?
| return none
let typeNames : Array Name ← ctx.runMetaM lctx <| getDotIdCompletionTypeNames expectedType
if typeNames.isEmpty then
return none
return some {
identifier := id.toString
openNamespaces := typeNames.map (.allExcept · #[])
env := ctx.env
determineInsertion decl :=
{
fullName := decl
edit := {
range := doc.meta.text.utf8RangeToLspRange ⟨pos, tailPos⟩
newText := decl.getString!
}
}
}
def computeQueries
(doc : EditableDocument)
(requestedPos : String.Pos)
: RequestM (Array Query) := do
let text := doc.meta.text
let some (stx, infoTree) := RequestM.findCmdDataAtPos doc requestedPos (includeStop := true) |>.get
| return #[]
let (completionPartitions, _) := findPrioritizedCompletionPartitionsAt text requestedPos stx infoTree
let mut queries := #[]
for partition in completionPartitions do
for (i, _) in partition do
let query? ←
match i.info with
| .id (stx := stx) (id := id) .. =>
pure <| computeIdQuery? doc i.ctx stx id
| .dot (termInfo := ti) .. =>
computeDotQuery? doc i.ctx ti
| .dotId stx id lctx expectedType? =>
computeDotIdQuery? doc i.ctx stx id lctx expectedType?
| _ =>
pure none
if let some query := query? then
queries := queries.push query
if ! queries.isEmpty then
break
return queries
def importAllUnknownIdentifiersProvider : Name := `unknownIdentifiers
def importAllUnknownIdentifiersCodeAction (params : CodeActionParams) (kind : String) : CodeAction := {
title := "Import all unambiguous unknown identifiers"
kind? := kind
data? := some <| toJson {
params,
providerName := importAllUnknownIdentifiersProvider
providerResultIndex := 0
: CodeActionResolveData
}
}
def handleUnknownIdentifierCodeAction
(id : JsonRpc.RequestID)
(params : CodeActionParams)
(requestedRange : String.Range)
: RequestM (Array CodeAction) := do
let rc ← read
let doc := rc.doc
let text := doc.meta.text
let queries ← computeQueries doc requestedRange.stop
if queries.isEmpty then
return #[]
let responseTask ← RequestM.sendServerRequest LeanQueryModuleParams LeanQueryModuleResponse "$/lean/queryModule" {
sourceRequestID := id
queries := queries.map (·.toLeanModuleQuery)
: LeanQueryModuleParams
}
let r ← ServerTask.waitAny [
responseTask.mapCheap Sum.inl,
rc.cancelTk.requestCancellationTask.mapCheap Sum.inr
]
let .inl (.success response) := r
| RequestM.checkCancelled
return #[]
let headerStx := doc.initSnap.stx
let importInsertionPos : Lsp.Position :=
match headerStx.getTailPos? with
| some headerTailPos => {
line := (text.utf8PosToLspPos headerTailPos |>.line) + 1
character := 0
}
| none => { line := 0, character := 0 }
let importInsertionRange : Lsp.Range := ⟨importInsertionPos, importInsertionPos⟩
let mut unknownIdentifierCodeActions := #[]
let mut hasUnambiguousImportCodeAction := false
let some result := response.queryResults[0]?
| return #[]
for query in queries, result in response.queryResults do
for ⟨mod, decl, isExactMatch⟩ in result do
let isDeclInEnv := query.env.contains decl
if ! isDeclInEnv && mod == query.env.mainModule then
-- Don't offer any code actions for identifiers defined further down in the same file
continue
let insertion := query.determineInsertion decl
if ! isDeclInEnv then
unknownIdentifierCodeActions := unknownIdentifierCodeActions.push {
title := s!"Import {insertion.fullName} from {mod}"
kind? := "quickfix"
edit? := WorkspaceEdit.ofTextDocumentEdit {
textDocument := doc.versionedIdentifier
edits := #[
{
range := importInsertionRange
newText := s!"import {mod}\n"
},
insertion.edit
]
}
}
if isExactMatch then
hasUnambiguousImportCodeAction := true
else
unknownIdentifierCodeActions := unknownIdentifierCodeActions.push {
title := s!"Change to {insertion.fullName}"
kind? := "quickfix"
edit? := WorkspaceEdit.ofTextDocumentEdit {
textDocument := doc.versionedIdentifier
edits := #[insertion.edit]
}
}
if hasUnambiguousImportCodeAction then
unknownIdentifierCodeActions := unknownIdentifierCodeActions.push <|
importAllUnknownIdentifiersCodeAction params "quickfix"
return unknownIdentifierCodeActions
def handleResolveImportAllUnknownIdentifiersCodeAction?
(id : JsonRpc.RequestID)
(action : CodeAction)
(unknownIdentifierRanges : Array String.Range)
: RequestM (Option CodeAction) := do
let rc ← read
let doc := rc.doc
let text := doc.meta.text
let queries ← unknownIdentifierRanges.flatMapM fun unknownIdentifierRange =>
computeQueries doc unknownIdentifierRange.stop
if queries.isEmpty then
return none
let responseTask ← RequestM.sendServerRequest LeanQueryModuleParams LeanQueryModuleResponse "$/lean/queryModule" {
sourceRequestID := id
queries := queries.map (·.toLeanModuleQuery)
: LeanQueryModuleParams
}
let .success response := responseTask.get
| return none
let headerStx := doc.initSnap.stx
let importInsertionPos : Lsp.Position :=
match headerStx.getTailPos? with
| some headerTailPos => {
line := (text.utf8PosToLspPos headerTailPos |>.line) + 1
character := 0
}
| none => { line := 0, character := 0 }
let importInsertionRange : Lsp.Range := ⟨importInsertionPos, importInsertionPos⟩
let mut edits : Array TextEdit := #[]
let mut imports : Std.HashSet Name := ∅
for q in queries, result in response.queryResults do
let some ⟨mod, decl, _⟩ := result.find? fun id =>
id.isExactMatch && ! q.env.contains id.decl
| continue
if mod == q.env.mainModule then
continue
let insertion := q.determineInsertion decl
if ! imports.contains mod then
edits := edits.push {
range := importInsertionRange
newText := s!"import {mod}\n"
}
edits := edits.push insertion.edit
imports := imports.insert mod
return some { action with
edit? := WorkspaceEdit.ofTextDocumentEdit {
textDocument := doc.versionedIdentifier
edits
}
}