Skip to content

Commit

Permalink
[clangd] Add support TextDocumentEdit.
Browse files Browse the repository at this point in the history
This is an initial patch to add TextDocumentEdit (versioned edits) support in
clangd, the scope is minimal:

- add and extend the corresponding protocol structures
- propagate the documentChanges for diagnostic codeactions (our motivated case)

Differential Revision: https://reviews.llvm.org/D148783
  • Loading branch information
hokein committed Apr 25, 2023
1 parent f30f34e commit 67e02b2
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 39 deletions.
54 changes: 46 additions & 8 deletions clang-tools-extra/clangd/ClangdLSPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace clang {
Expand Down Expand Up @@ -95,6 +96,26 @@ CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
return CA;
}

/// Convert from Fix to LSP CodeAction.
CodeAction toCodeAction(const Fix &F, const URIForFile &File,
const std::optional<int64_t> &Version,
bool SupportsDocumentChanges) {
CodeAction Action;
Action.title = F.Message;
Action.kind = std::string(CodeAction::QUICKFIX_KIND);
Action.edit.emplace();
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;
}

void adjustSymbolKinds(llvm::MutableArrayRef<DocumentSymbol> Syms,
SymbolKindBitset Kinds) {
for (auto &S : Syms) {
Expand Down Expand Up @@ -487,6 +508,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
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;
Expand Down Expand Up @@ -761,8 +783,10 @@ void ClangdLSPServer::onCommandApplyTweak(const TweakArgs &Args,
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().
Expand Down Expand Up @@ -833,8 +857,12 @@ void ClangdLSPServer::onRename(const RenameParams &Params,
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);
Expand Down Expand Up @@ -984,8 +1012,8 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
std::vector<CodeAction> 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));
for (auto &CA : getFixes(File.file(), D)) {
FixIts.push_back(CA);
FixIts.back().diagnostics = {D};
}
}
Expand Down Expand Up @@ -1663,8 +1691,8 @@ void ClangdLSPServer::profile(MemoryTree &MT) const {
Server->profile(MT.child("clangd_server"));
}

std::vector<Fix> ClangdLSPServer::getFixes(llvm::StringRef File,
const clangd::Diagnostic &D) {
std::vector<CodeAction> ClangdLSPServer::getFixes(llvm::StringRef File,
const clangd::Diagnostic &D) {
std::lock_guard<std::mutex> Lock(FixItsMutex);
auto DiagToFixItsIter = FixItsMap.find(File);
if (DiagToFixItsIter == FixItsMap.end())
Expand Down Expand Up @@ -1710,8 +1738,18 @@ void ClangdLSPServer::onDiagnosticsReady(PathRef File, llvm::StringRef Version,
for (auto &Diag : Diagnostics) {
toLSPDiags(Diag, Notification.uri, DiagOpts,
[&](clangd::Diagnostic Diag, llvm::ArrayRef<Fix> Fixes) {
auto &FixItsForDiagnostic = LocalFixIts[Diag];
llvm::copy(Fixes, std::back_inserter(FixItsForDiagnostic));
std::vector<CodeAction> CodeActions;
for (const auto &Fix : Fixes)
CodeActions.push_back(toCodeAction(Fix, Notification.uri,
Notification.version,
SupportsDocumentChanges));

if (DiagOpts.EmbedFixesInDiagnostics) {
Diag.codeActions.emplace(CodeActions);
if (Diag.codeActions->size() == 1)
Diag.codeActions->front().isPreferred = true;
}
LocalFixIts[Diag] = std::move(CodeActions);
Notification.diagnostics.push_back(std::move(Diag));
});
}
Expand Down
12 changes: 9 additions & 3 deletions clang-tools-extra/clangd/ClangdLSPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "llvm/Support/JSON.h"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <vector>
Expand Down Expand Up @@ -197,7 +198,8 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
Callback<llvm::json::Value> Reply);

void bindMethods(LSPBinder &, const ClientCapabilities &Caps);
std::vector<Fix> getFixes(StringRef File, const clangd::Diagnostic &D);
std::vector<CodeAction> 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
Expand Down Expand Up @@ -231,10 +233,12 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
std::atomic<bool> IsBeingDestroyed = {false};

std::mutex FixItsMutex;
typedef std::map<clangd::Diagnostic, std::vector<Fix>, LSPDiagnosticCompare>
typedef std::map<clangd::Diagnostic, std::vector<CodeAction>,
LSPDiagnosticCompare>
DiagnosticToReplacementMap;
/// Caches FixIts per file and diagnostics
llvm::StringMap<DiagnosticToReplacementMap> FixItsMap;
llvm::StringMap<DiagnosticToReplacementMap>
FixItsMap;
// Last semantic-tokens response, for incremental requests.
std::mutex SemanticTokensMutex;
llvm::StringMap<SemanticTokens> LastSemanticTokens;
Expand Down Expand Up @@ -271,6 +275,8 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
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.
Expand Down
16 changes: 0 additions & 16 deletions clang-tools-extra/clangd/Diagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -423,15 +423,6 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Diag &D) {
return OS;
}

CodeAction toCodeAction(const Fix &F, const URIForFile &File) {
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()};
return Action;
}

Diag toDiag(const llvm::SMDiagnostic &D, Diag::DiagSource Source) {
Diag Result;
Result.Message = D.getMessage().str();
Expand Down Expand Up @@ -499,13 +490,6 @@ void toLSPDiags(
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;

Expand Down
3 changes: 0 additions & 3 deletions clang-tools-extra/clangd/Diagnostics.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ void toLSPDiags(
const Diag &D, const URIForFile &File, const ClangdDiagnosticOptions &Opts,
llvm::function_ref<void(clangd::Diagnostic, llvm::ArrayRef<Fix>)> OutFn);

/// Convert from Fix to LSP CodeAction.
CodeAction toCodeAction(const Fix &D, const URIForFile &File);

/// Convert from clang diagnostic level to LSP severity.
int getSeverity(DiagnosticsEngine::Level L);

Expand Down
32 changes: 27 additions & 5 deletions clang-tools-extra/clangd/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ llvm::json::Value toJSON(const TextEdit &P) {
};
}

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);
Expand Down Expand Up @@ -444,6 +455,10 @@ bool fromJSON(const llvm::json::Value &Params, ClientCapabilities &R,
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"))
Expand Down Expand Up @@ -717,7 +732,8 @@ bool fromJSON(const llvm::json::Value &Params, CodeActionParams &R,
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,
Expand Down Expand Up @@ -863,10 +879,16 @@ llvm::json::Value toJSON(const DocumentSymbol &S) {
}

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,
Expand Down
28 changes: 24 additions & 4 deletions clang-tools-extra/clangd/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,17 @@ bool fromJSON(const llvm::json::Value &, TextEdit &, llvm::json::Path);
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<TextEdit> 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;
Expand Down Expand Up @@ -517,6 +528,9 @@ struct ClientCapabilities {
/// 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;
Expand Down Expand Up @@ -970,12 +984,18 @@ struct CodeActionParams {
};
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<std::string, std::vector<TextEdit>> changes;

/// Note: "documentChanges" is not currently used because currently there is
/// no support for versioned edits.
std::optional<std::map<std::string, std::vector<TextEdit>>> 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<std::vector<TextDocumentEdit>> documentChanges;
};
bool fromJSON(const llvm::json::Value &, WorkspaceEdit &, llvm::json::Path);
llvm::json::Value toJSON(const WorkspaceEdit &WE);
Expand Down

0 comments on commit 67e02b2

Please sign in to comment.