/- 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 } }