Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -155,6 +155,8 @@ RealFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; + /// Options used for diagnostics. + ClangdDiagnosticOptions DiagOpts; /// The supported kinds of the client. SymbolKindBitset SupportedSymbolKinds; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -82,6 +82,8 @@ CCOpts.EnableSnippets = Params.capabilities.textDocument.completion.completionItem.snippetSupport; + DiagOpts.EmbedFixesInDiagnostics = + Params.capabilities.textDocument.publishDiagnostics.clangdFixSupport; if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && Params.capabilities.workspace->symbol->symbolKind) { @@ -486,11 +488,25 @@ DiagnosticToReplacementMap LocalFixIts; // Temporary storage for (auto &Diag : Diagnostics) { toLSPDiags(Diag, [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { - DiagnosticsJSON.push_back(json::Object{ + json::Object LSPDiag({ {"range", Diag.range}, {"severity", Diag.severity}, {"message", Diag.message}, }); + // LSP extension: embed the fixes in the diagnostic. + if (DiagOpts.EmbedFixesInDiagnostics && !Fixes.empty()) { + json::Array ClangdFixes; + for (const auto &Fix : Fixes) { + WorkspaceEdit WE; + URIForFile URI{File}; + WE.changes = {{URI.uri(), std::vector(Fix.Edits.begin(), + Fix.Edits.end())}}; + ClangdFixes.push_back( + json::Object{{"edit", toJSON(WE)}, {"title", Fix.Message}}); + } + LSPDiag["clangd.fixes"] = std::move(ClangdFixes); + } + DiagnosticsJSON.push_back(std::move(LSPDiag)); auto &FixItsForDiagnostic = LocalFixIts[Diag]; std::copy(Fixes.begin(), Fixes.end(), Index: clangd/Diagnostics.h =================================================================== --- clangd/Diagnostics.h +++ clangd/Diagnostics.h @@ -23,6 +23,12 @@ namespace clang { namespace clangd { +struct ClangdDiagnosticOptions { + /// If true, Clangd uses an LSP extension to embed the fixes with the + /// diagnostics that are sent to the client. + bool EmbedFixesInDiagnostics = false; +}; + /// Contains basic information about a diagnostic. struct DiagBase { std::string Message; Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -247,6 +247,18 @@ }; bool fromJSON(const llvm::json::Value &, CompletionClientCapabilities &); +struct PublishDiagnosticsClientCapabilities { + // Whether the client accepts diagnostics with related information. + // NOTE: not used by clangd at the moment. + // bool relatedInformation; + + /// Whether the client accepts diagnostics with fixes attached using the + /// "clangd.fixes" extension. + bool clangdFixSupport = false; +}; +bool fromJSON(const llvm::json::Value &, + PublishDiagnosticsClientCapabilities &); + /// A symbol kind. enum class SymbolKind { File = 1, @@ -313,6 +325,9 @@ struct TextDocumentClientCapabilities { /// Capabilities specific to the `textDocument/completion` CompletionClientCapabilities completion; + + /// Capabilities specific to the 'textDocument/publishDiagnostics' + PublishDiagnosticsClientCapabilities publishDiagnostics; }; bool fromJSON(const llvm::json::Value &, TextDocumentClientCapabilities &); Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -178,6 +178,15 @@ return true; } +bool fromJSON(const llvm::json::Value &Params, + PublishDiagnosticsClientCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("clangdFixSupport", R.clangdFixSupport); + return true; +} + bool fromJSON(const json::Value &E, SymbolKind &Out) { if (auto T = E.getAsInteger()) { if (*T < static_cast(SymbolKind::File) || @@ -240,6 +249,7 @@ if (!O) return false; O.map("completion", R.completion); + O.map("publishDiagnostics", R.publishDiagnostics); return true; } Index: test/clangd/fixits-embed-in-diagnostic.test =================================================================== --- /dev/null +++ test/clangd/fixits-embed-in-diagnostic.test @@ -0,0 +1,66 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"publishDiagnostics":{"clangdFixSupport":true}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"struct Point {}; union Point p;"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "clangd.fixes": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "struct", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 22, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "title": "change 'union' to 'struct'" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "message": "Use of 'Point' with tag type that does not match previous declaration\n\nfoo.c:1:8: note: previous use is here", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 22, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Previous use is here\n\nfoo.c:1:18: error: use of 'Point' with tag type that does not match previous declaration", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 3 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"}