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']*" ```
331 lines
11 KiB
Text
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
|
|
}
|
|
}
|