diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -21,6 +21,7 @@ #include "llvm/Support/JSON.h" #include #include +#include #include #include #include @@ -197,7 +198,8 @@ Callback Reply); void bindMethods(LSPBinder &, const ClientCapabilities &Caps); - std::vector getFixes(StringRef File, const clangd::Diagnostic &D); + std::pair, std::vector> + getFixes(StringRef File, const clangd::Diagnostic &D); /// Checks if completion request should be ignored. We need this due to the /// limitation of the LSP. Per LSP, a client sends requests for all "trigger @@ -234,7 +236,9 @@ typedef std::map, LSPDiagnosticCompare> DiagnosticToReplacementMap; /// Caches FixIts per file and diagnostics - llvm::StringMap FixItsMap; + llvm::StringMap< + std::pair, DiagnosticToReplacementMap>> + FixItsMap; // Last semantic-tokens response, for incremental requests. std::mutex SemanticTokensMutex; llvm::StringMap LastSemanticTokens; @@ -271,6 +275,8 @@ MarkupKind HoverContentFormat = MarkupKind::PlainText; /// Whether the client supports offsets for parameter info labels. bool SupportsOffsetsInSignatureHelp = false; + /// Whether the client supports the versioned document changes. + bool SupportsDocumentChanges = false; std::mutex BackgroundIndexProgressMutex; enum class BackgroundIndexProgress { // Client doesn't support reporting progress. No transitions possible. diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -487,6 +487,7 @@ Params.capabilities.HierarchicalDocumentSymbol; SupportsReferenceContainer = Params.capabilities.ReferenceContainer; SupportFileStatus = Params.initializationOptions.FileStatus; + SupportsDocumentChanges = Params.capabilities.DocumentChanges; HoverContentFormat = Params.capabilities.HoverContentFormat; Opts.LineFoldingOnly = Params.capabilities.LineFoldingOnly; SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp; @@ -761,8 +762,10 @@ return Reply(std::move(Err)); WorkspaceEdit WE; + // FIXME: use documentChanges when SupportDocumentChanges is true. + WE.changes.emplace(); for (const auto &It : R->ApplyEdits) { - WE.changes[URI::createFile(It.first()).toString()] = + (*WE.changes)[URI::createFile(It.first()).toString()] = It.second.asTextEdits(); } // ApplyEdit will take care of calling Reply(). @@ -833,8 +836,12 @@ if (auto Err = validateEdits(*Server, R->GlobalChanges)) return Reply(std::move(Err)); WorkspaceEdit Result; + // FIXME: use documentChanges if SupportDocumentChanges is + // true. + Result.changes.emplace(); for (const auto &Rep : R->GlobalChanges) { - Result.changes[URI::createFile(Rep.first()).toString()] = + (*Result + .changes)[URI::createFile(Rep.first()).toString()] = Rep.second.asTextEdits(); } Reply(Result); @@ -984,8 +991,10 @@ std::vector FixIts; if (KindAllowed(CodeAction::QUICKFIX_KIND)) { for (const Diagnostic &D : Params.context.diagnostics) { - for (auto &F : getFixes(File.file(), D)) { - FixIts.push_back(toCodeAction(F, Params.textDocument.uri)); + const auto& [Version, Fixes] = getFixes(File.file(), D); + for (auto &F : Fixes) { + FixIts.push_back(toCodeAction(F, Params.textDocument.uri, Version, + SupportsDocumentChanges)); FixIts.back().diagnostics = {D}; } } @@ -1663,19 +1672,19 @@ Server->profile(MT.child("clangd_server")); } -std::vector ClangdLSPServer::getFixes(llvm::StringRef File, - const clangd::Diagnostic &D) { +std::pair, std::vector> +ClangdLSPServer::getFixes(llvm::StringRef File, const clangd::Diagnostic &D) { std::lock_guard Lock(FixItsMutex); auto DiagToFixItsIter = FixItsMap.find(File); if (DiagToFixItsIter == FixItsMap.end()) return {}; const auto &DiagToFixItsMap = DiagToFixItsIter->second; - auto FixItsIter = DiagToFixItsMap.find(D); - if (FixItsIter == DiagToFixItsMap.end()) + auto FixItsIter = DiagToFixItsMap.second.find(D); + if (FixItsIter == DiagToFixItsMap.second.end()) return {}; - return FixItsIter->second; + return {DiagToFixItsMap.first, FixItsIter->second}; } // A completion request is sent when the user types '>' or ':', but we only @@ -1710,8 +1719,18 @@ for (auto &Diag : Diagnostics) { toLSPDiags(Diag, Notification.uri, DiagOpts, [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { + if (DiagOpts.EmbedFixesInDiagnostics && !Fixes.empty()) { + Diag.codeActions.emplace(); + for (const auto &Fix : Fixes) + Diag.codeActions->push_back(toCodeAction( + Fix, Notification.uri, Notification.version, + SupportsDocumentChanges)); + if (Diag.codeActions->size() == 1) + Diag.codeActions->front().isPreferred = true; + } auto &FixItsForDiagnostic = LocalFixIts[Diag]; llvm::copy(Fixes, std::back_inserter(FixItsForDiagnostic)); + Notification.diagnostics.push_back(std::move(Diag)); }); } @@ -1719,7 +1738,7 @@ // Cache FixIts { std::lock_guard Lock(FixItsMutex); - FixItsMap[File] = LocalFixIts; + FixItsMap[File] = {Notification.version, LocalFixIts}; } // Send a notification to the LSP client. diff --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h --- a/clang-tools-extra/clangd/Diagnostics.h +++ b/clang-tools-extra/clangd/Diagnostics.h @@ -121,7 +121,9 @@ llvm::function_ref)> OutFn); /// Convert from Fix to LSP CodeAction. -CodeAction toCodeAction(const Fix &D, const URIForFile &File); +CodeAction toCodeAction(const Fix &D, const URIForFile &File, + const std::optional &Version, + bool SupportsDocumentChanges); /// Convert from clang diagnostic level to LSP severity. int getSeverity(DiagnosticsEngine::Level L); diff --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp --- a/clang-tools-extra/clangd/Diagnostics.cpp +++ b/clang-tools-extra/clangd/Diagnostics.cpp @@ -423,12 +423,22 @@ return OS; } -CodeAction toCodeAction(const Fix &F, const URIForFile &File) { +CodeAction toCodeAction(const Fix &F, const URIForFile &File, + const std::optional &Version, + bool SupportsDocumentChanges) { CodeAction Action; Action.title = F.Message; Action.kind = std::string(CodeAction::QUICKFIX_KIND); Action.edit.emplace(); - Action.edit->changes[File.uri()] = {F.Edits.begin(), F.Edits.end()}; + if (!SupportsDocumentChanges) { + Action.edit->changes.emplace(); + (*Action.edit->changes)[File.uri()] = {F.Edits.begin(), F.Edits.end()}; + } else { + Action.edit->documentChanges.emplace(); + TextDocumentEdit& Edit = Action.edit->documentChanges->emplace_back(); + Edit.textDocument = VersionedTextDocumentIdentifier{{File}, Version}; + Edit.edits = {F.Edits.begin(), F.Edits.end()}; + } return Action; } @@ -499,13 +509,6 @@ case Diag::Unknown: break; } - if (Opts.EmbedFixesInDiagnostics) { - Main.codeActions.emplace(); - for (const auto &Fix : D.Fixes) - Main.codeActions->push_back(toCodeAction(Fix, File)); - if (Main.codeActions->size() == 1) - Main.codeActions->front().isPreferred = true; - } if (Opts.SendDiagnosticCategory && !D.Category.empty()) Main.category = D.Category; diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -254,6 +254,17 @@ llvm::json::Value toJSON(const TextEdit &); llvm::raw_ostream &operator<<(llvm::raw_ostream &, const TextEdit &); +struct TextDocumentEdit { + /// The text document to change. + VersionedTextDocumentIdentifier textDocument; + + /// The edits to be applied. + /// FIXME: support the AnnotatedTextEdit variant. + std::vector edits; +}; +bool fromJSON(const llvm::json::Value &, TextDocumentEdit &, llvm::json::Path); +llvm::json::Value toJSON(const TextDocumentEdit &); + struct TextDocumentItem { /// The text document's URI. URIForFile uri; @@ -517,6 +528,9 @@ /// server to the client. bool SemanticTokenRefreshSupport = false; + /// The client supports versioned document changes for WorkspaceEdit. + bool DocumentChanges = false; + /// Whether the client supports the textDocument/inactiveRegions /// notification. This is a clangd extension. bool InactiveRegions = false; @@ -970,12 +984,18 @@ }; bool fromJSON(const llvm::json::Value &, CodeActionParams &, llvm::json::Path); +/// The edit should either provide changes or documentChanges. If the client +/// can handle versioned document edits and if documentChanges are present, +/// the latter are preferred over changes. struct WorkspaceEdit { /// Holds changes to existing resources. - std::map> changes; - - /// Note: "documentChanges" is not currently used because currently there is - /// no support for versioned edits. + std::optional>> changes; + /// Versioned document edits. + /// + /// If a client neither supports `documentChanges` nor + /// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s + /// using the `changes` property are supported. + std::optional> documentChanges; }; bool fromJSON(const llvm::json::Value &, WorkspaceEdit &, llvm::json::Path); llvm::json::Value toJSON(const WorkspaceEdit &WE); diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -197,6 +197,17 @@ }; } +bool fromJSON(const llvm::json::Value &Params, TextDocumentEdit &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O && O.map("textDocument", R.textDocument) && O.map("edits", R.edits); +} +llvm::json::Value toJSON(const TextDocumentEdit &P) { + llvm::json::Object Result{{"textDocument", P.textDocument}, + {"edits", P.edits}}; + return Result; +} + llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const TextEdit &TE) { OS << TE.range << " => \""; llvm::printEscapedString(TE.newText, OS); @@ -444,6 +455,10 @@ if (auto RefreshSupport = SemanticTokens->getBoolean("refreshSupport")) R.SemanticTokenRefreshSupport = *RefreshSupport; } + if (auto *WorkspaceEdit = Workspace->getObject("workspaceEdit")) { + if (auto DocumentChanges = WorkspaceEdit->getBoolean("documentChanges")) + R.DocumentChanges = *DocumentChanges; + } } if (auto *Window = O->getObject("window")) { if (auto WorkDoneProgress = Window->getBoolean("workDoneProgress")) @@ -717,7 +732,7 @@ bool fromJSON(const llvm::json::Value &Params, WorkspaceEdit &R, llvm::json::Path P) { llvm::json::ObjectMapper O(Params, P); - return O && O.map("changes", R.changes); + return O && O.map("changes", R.changes) && O.map("documentChanges", R.documentChanges); } bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R, @@ -863,10 +878,16 @@ } llvm::json::Value toJSON(const WorkspaceEdit &WE) { - llvm::json::Object FileChanges; - for (auto &Change : WE.changes) - FileChanges[Change.first] = llvm::json::Array(Change.second); - return llvm::json::Object{{"changes", std::move(FileChanges)}}; + llvm::json::Object Result; + if (WE.changes) { + llvm::json::Object FileChanges; + for (auto &Change : *WE.changes) + FileChanges[Change.first] = llvm::json::Array(Change.second); + Result["changes"] = std::move(FileChanges); + } + if (WE.documentChanges) + Result["documentChanges"] = *WE.documentChanges; + return Result; } bool fromJSON(const llvm::json::Value &Params, TweakArgs &A,