61 changes: 61 additions & 0 deletions clang-tools-extra/clangd/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ struct Position {
/// Character offset on a line in a document (zero-based).
int character;

friend bool operator==(const Position &LHS, const Position &RHS) {
return std::tie(LHS.line, LHS.character) ==
std::tie(RHS.line, RHS.character);
}
friend bool operator<(const Position &LHS, const Position &RHS) {
return std::tie(LHS.line, LHS.character) <
std::tie(RHS.line, RHS.character);
}

static llvm::Optional<Position> parse(llvm::yaml::MappingNode *Params);
static std::string unparse(const Position &P);
};
Expand All @@ -55,6 +64,13 @@ struct Range {
/// The range's end position.
Position end;

friend bool operator==(const Range &LHS, const Range &RHS) {
return std::tie(LHS.start, LHS.end) == std::tie(RHS.start, RHS.end);
}
friend bool operator<(const Range &LHS, const Range &RHS) {
return std::tie(LHS.start, LHS.end) < std::tie(RHS.start, RHS.end);
}

static llvm::Optional<Range> parse(llvm::yaml::MappingNode *Params);
static std::string unparse(const Range &P);
};
Expand Down Expand Up @@ -172,6 +188,51 @@ struct DocumentFormattingParams {
parse(llvm::yaml::MappingNode *Params);
};

struct Diagnostic {
/// The range at which the message applies.
Range range;

/// The diagnostic's severity. Can be omitted. If omitted it is up to the
/// client to interpret diagnostics as error, warning, info or hint.
int severity;

/// The diagnostic's message.
std::string message;

friend bool operator==(const Diagnostic &LHS, const Diagnostic &RHS) {
return std::tie(LHS.range, LHS.severity, LHS.message) ==
std::tie(RHS.range, RHS.severity, RHS.message);
}
friend bool operator<(const Diagnostic &LHS, const Diagnostic &RHS) {
return std::tie(LHS.range, LHS.severity, LHS.message) <
std::tie(RHS.range, RHS.severity, RHS.message);
}

static llvm::Optional<Diagnostic> parse(llvm::yaml::MappingNode *Params);
};

struct CodeActionContext {
/// An array of diagnostics.
std::vector<Diagnostic> diagnostics;

static llvm::Optional<CodeActionContext>
parse(llvm::yaml::MappingNode *Params);
};

struct CodeActionParams {
/// The document in which the command was invoked.
TextDocumentIdentifier textDocument;

/// The range for which the command was invoked.
Range range;

/// Context carrying additional information.
CodeActionContext context;

static llvm::Optional<CodeActionParams>
parse(llvm::yaml::MappingNode *Params);
};

} // namespace clangd
} // namespace clang

Expand Down
64 changes: 52 additions & 12 deletions clang-tools-extra/clangd/ProtocolHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//===----------------------------------------------------------------------===//

#include "ProtocolHandlers.h"
#include "ASTManager.h"
#include "DocumentStore.h"
#include "clang/Format/Format.h"
using namespace clang;
Expand Down Expand Up @@ -58,18 +59,9 @@ static Position offsetToPosition(StringRef Code, size_t Offset) {
return {Lines, Cols};
}

static std::string formatCode(StringRef Code, StringRef Filename,
ArrayRef<tooling::Range> Ranges, StringRef ID) {
// Call clang-format.
// FIXME: Don't ignore style.
format::FormatStyle Style = format::getLLVMStyle();
// On windows FileManager doesn't like file://. Just strip it, clang-format
// doesn't need it.
Filename.consume_front("file://");
tooling::Replacements Replacements =
format::reformat(Style, Code, Ranges, Filename);

// Now turn the replacements into the format specified by the Language Server
template <typename T>
static std::string replacementsToEdits(StringRef Code, const T &Replacements) {
// Turn the replacements into the format specified by the Language Server
// Protocol. Fuse them into one big JSON array.
std::string Edits;
for (auto &R : Replacements) {
Expand All @@ -83,6 +75,21 @@ static std::string formatCode(StringRef Code, StringRef Filename,
if (!Edits.empty())
Edits.pop_back();

return Edits;
}

static std::string formatCode(StringRef Code, StringRef Filename,
ArrayRef<tooling::Range> Ranges, StringRef ID) {
// Call clang-format.
// FIXME: Don't ignore style.
format::FormatStyle Style = format::getLLVMStyle();
// On windows FileManager doesn't like file://. Just strip it, clang-format
// doesn't need it.
Filename.consume_front("file://");
tooling::Replacements Replacements =
format::reformat(Style, Code, Ranges, Filename);

std::string Edits = replacementsToEdits(Code, Replacements);
return R"({"jsonrpc":"2.0","id":)" + ID.str() +
R"(,"result":[)" + Edits + R"(]})";
}
Expand Down Expand Up @@ -138,3 +145,36 @@ void TextDocumentFormattingHandler::handleMethod(
writeMessage(formatCode(Code, DFP->textDocument.uri,
{clang::tooling::Range(0, Code.size())}, ID));
}

void CodeActionHandler::handleMethod(llvm::yaml::MappingNode *Params,
StringRef ID) {
auto CAP = CodeActionParams::parse(Params);
if (!CAP) {
Output.log("Failed to decode CodeActionParams!\n");
return;
}

// We provide a code action for each diagnostic at the requested location
// which has FixIts available.
std::string Code = AST.getStore().getDocument(CAP->textDocument.uri);
std::string Commands;
for (Diagnostic &D : CAP->context.diagnostics) {
std::vector<clang::tooling::Replacement> Fixes = AST.getFixIts(D);
std::string Edits = replacementsToEdits(Code, Fixes);

if (!Edits.empty())
Commands +=
R"({"title":"Apply FixIt ')" + llvm::yaml::escape(D.message) +
R"('", "command": "clangd.applyFix", "arguments": [")" +
llvm::yaml::escape(CAP->textDocument.uri) +
R"(", [)" + Edits +
R"(]]},)";
}
if (!Commands.empty())
Commands.pop_back();

writeMessage(
R"({"jsonrpc":"2.0","id":)" + ID.str() +
R"(, "result": [)" + Commands +
R"(]})");
}
14 changes: 13 additions & 1 deletion clang-tools-extra/clangd/ProtocolHandlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

namespace clang {
namespace clangd {
class ASTManager;
class DocumentStore;

struct InitializeHandler : Handler {
Expand All @@ -34,7 +35,8 @@ struct InitializeHandler : Handler {
"textDocumentSync": 1,
"documentFormattingProvider": true,
"documentRangeFormattingProvider": true,
"documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]}
"documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]},
"codeActionProvider": true
}}})");
}
};
Expand Down Expand Up @@ -102,6 +104,16 @@ struct TextDocumentFormattingHandler : Handler {
DocumentStore &Store;
};

struct CodeActionHandler : Handler {
CodeActionHandler(JSONOutput &Output, ASTManager &AST)
: Handler(Output), AST(AST) {}

void handleMethod(llvm::yaml::MappingNode *Params, StringRef ID) override;

private:
ASTManager &AST;
};

} // namespace clangd
} // namespace clang

Expand Down
18 changes: 17 additions & 1 deletion clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,25 @@ export function activate(context: vscode.ExtensionContext) {

const clangdClient = new vscodelc.LanguageClient('Clang Language Server', serverOptions, clientOptions);

function applyTextEdits(uri: string, edits: vscodelc.TextEdit[]) {
let textEditor = vscode.window.activeTextEditor;

if (textEditor && textEditor.document.uri.toString() === uri) {
textEditor.edit(mutator => {
for (const edit of edits) {
mutator.replace(vscodelc.Protocol2Code.asRange(edit.range), edit.newText);
}
}).then((success) => {
if (!success) {
vscode.window.showErrorMessage('Failed to apply fixes to the document.');
}
});
}
}

console.log('Clang Language Server is now active!');

const disposable = clangdClient.start();

context.subscriptions.push(disposable);
context.subscriptions.push(disposable, vscode.commands.registerCommand('clangd.applyFix', applyTextEdits));
}
22 changes: 22 additions & 0 deletions clang-tools-extra/test/clangd/fixits.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# RUN: clangd -run-synchronously < %s | FileCheck %s
# It is absolutely vital that this file has CRLF line endings.
#
Content-Length: 125

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}
#
Content-Length: 180

{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}}
#
# CHECK: {"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///foo.c","diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}
#
Content-Length: 746

{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"file:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}}
#
# CHECK: {"jsonrpc":"2.0","id":2, "result": [{"title":"Apply FixIt 'place parentheses around the assignment to silence this warning'", "command": "clangd.applyFix", "arguments": ["file:///foo.c", [{"range": {"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 32}}, "newText": "("},{"range": {"start": {"line": 0, "character": 37}, "end": {"line": 0, "character": 37}}, "newText": ")"}]]},{"title":"Apply FixIt 'use '==' to turn this assignment into an equality comparison'", "command": "clangd.applyFix", "arguments": ["file:///foo.c", [{"range": {"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}}, "newText": "=="}]]}]
#
Content-Length: 44

{"jsonrpc":"2.0","id":3,"method":"shutdown"}
5 changes: 3 additions & 2 deletions clang-tools-extra/test/clangd/formatting.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
Content-Length: 125

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}
# CHECK: Content-Length: 294
# CHECK: Content-Length: 332
# CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{
# CHECK: "textDocumentSync": 1,
# CHECK: "documentFormattingProvider": true,
# CHECK: "documentRangeFormattingProvider": true,
# CHECK: "documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]}
# CHECK: "documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]},
# CHECK: "codeActionProvider": true
# CHECK: }}}
#
Content-Length: 193
Expand Down