1. 什么是clangd
clangd是llvm项目推出的C++语言服务器,通过LSP(Language Server Protocal)协议向编辑器如vscode/vim/emacs提供语法补全、错误检测、跳转、格式化等等功能。
2. vscode-clangd extension
vscode-clangd extension 是vscode的一个插件
3. 编译clangd
git clone https://github.com/llvm/llvm-project.git
export LLVM_ROOT=llvm_path
export LD_LIBRARY_PATH=gcc-9.3.0/lib64:$LD_LIBRARY_PATH
export PATH=gcc-9.3.0/bin:$PATH
mkdir build && cd build
cmake $LLVM_ROOT/llvm/ -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra"
cmake --build $LLVM_ROOT/build --target clangd
4. 通信机制
1. Entry point and JSON-RPC
The clangd binary itself has its main() entrypoint in tool/ClangdMain.cpp. This mostly parses flags and hooks ClangdLSPServer up to its dependencies.
One vital dependency is JSONTransport which speaks the JSON-RPC protocol over stdin/stdout. LSP is layered on top of this Transport abstraction. (There’s also an Apple XPC transport in the xpc/ directory).
We call ClangdLSPServer.run() to start the loop, and it synchronously processes messages until the client disconnects. Calls to the large non-threadsafe singletons (ClangdLSPServer, ClangdServer, TUScheduler) all happen on the main thread.
2. Language Server Protocol
ClangdLSPServer handles the LSP protocol details. Incoming requests are routed to some method on this class using a lookup table, and then implemented by dispatching them to the contained ClangdServer.
The incoming JSON requests are mapped onto structs defined in Protocol.h. In the simplest cases these are just forwarded to the appropriate method on ClangdServer - we use the LSP structs as vocabulary types for most things.
In other cases there’s some gap between LSP and what seems to be a sensible C++ API, so ClangdLSPServer has some real work to do.
3. ClangdServer and TUScheduler
The ClangdServer class is best thought of as the C++ API to clangd. Features tend to be implemented as stateless, synchronous functions (“give me hover information from this AST at offset 25”). ClangdServer exposes them as stateful, asynchronous functions (“compute hover information for the latest version of Foo.cpp at offset 25, call back when done”) which is the LSP model.
TUScheduler is responsible for keeping track of the latest version of each file, building and caching ASTs and preambles as inputs, and providing threads to run requests on in an appropriate sequence. (More details in threads and request handling). It also pushes certain events to ClangdServer via ParsingCallbacks, to allow emitting diagnostics and indexing ASTs.
ClangdServer is fairly mechanical for the most part. The features are implemented in various other files, and the scheduling and AST building is done by TUScheduler, so largely it just binds these together. TUScheduler doesn’t know about particular features (except diagnostics).
4. Compile commands
Like other clang-based tools, clangd uses clang’s command-line syntax as its interface to configure parse options (like include directories). The arguments are obtained from a tooling::CompilationDatabase, typically built by reading compile_commands.json from a nearby directory. GlobalCompilationDatabase is responsible for finding and caching such databases, and for providing “fallback” commands when none are found.
5. RPC API Demo
1. PRC API in clangd server
// Add new function
// ClangdLSPServer.h
void onSimplifiedAst(const HLSASTParams &, Callback<std::optional<ASTNode>>);
// ClangdLSPServer.cpp
void ClangdLSPServer::onSimplifiedAst(const HLSASTParams &Params,
Callback<std::optional<ASTNode>> CB) {
log("test_clangd: onSimplifiedAst: configPath:{0}", Params.configPath);
Server->getAST(Params.textDocument.uri.file(), std::nullopt, std::move(CB));
}
// Add bind method in ClangdLSPServer.cpp
Bind.method("textDocument/simplifiedAst", this, &ClangdLSPServer::onSimplifiedAst);
2. RPC API in clangd client
// test-ast.ts file
import * as vscode from "vscode";
import * as vscodelc from "vscode-languageclient/node";
import { ClangdContext } from "./clangd-context";
export function activate(context: ClangdContext) {
const feature = new HLSASTFeature(context.subscriptions, context.client);
context.client.registerFeature(feature);
}
// 入参
interface HLSASTParams {
textDocument: vscodelc.TextDocumentIdentifier;
configPath?: string;
}
// 返回值
interface HLSASTNode {
role: string; // e.g. expression
kind: string; // e.g. BinaryOperator
detail?: string; // e.g. ||
arcana?: string; // e.g. BinaryOperator <0x12345> <col:12, col:1> 'bool' '||'
children?: Array<HLSASTNode>;
bodyRange?: vscodelc.Range;
range?: vscodelc.Range;
}
const RequestType = new vscodelc.RequestType<
HLSASTParams,
HLSASTNode | null,
void
>("textDocument/simplifiedAst");
class HLSASTFeature implements vscodelc.StaticFeature {
private subscriptions: vscode.Disposable[];
private client!: vscodelc.LanguageClient;
constructor(
subscriptions: vscode.Disposable[],
client: vscodelc.LanguageClient
) {
this.subscriptions = subscriptions;
this.client = client;
}
fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {}
initialize(
capabilities: vscodelc.ServerCapabilities,
_documentSelector: vscodelc.DocumentSelector | undefined
) {
try {
const cmd = vscode.commands.registerCommand("clangd.hlsAst", async () => {
vscode.window.showInformationMessage("vscode: test_clangd_simplified_ast");
const editor = vscode.window.activeTextEditor;
if (!editor) {
return null;
}
const converter = this.client.code2ProtocolConverter;
const item = await this.client.sendRequest(RequestType, {
textDocument: converter.asTextDocumentIdentifier(editor.document),
configPath: "/proj/test/test1/tetst2",
});
if (!item) {
vscode.window.showInformationMessage("vscode: no astnode found");
} else {
vscode.window.showInformationMessage("vscode simplified_ast: " + item.kind);
}
return item;
});
const cmd1 = vscode.commands.registerCommand("clangd.simplifiedAst", async (filePath: string) => {
const doc = await vscode.workspace.openTextDocument(filePath);
const converter = this.client.code2ProtocolConverter;
const item = await this.client.sendRequest(RequestType, {
textDocument: converter.asTextDocumentIdentifier(doc),
});
return item;
});
this.subscriptions.push(cmd, cmd1);
} catch (error) {}
}
getState(): vscodelc.FeatureState {
return { kind: "static" };
}
dispose() {}
}
// activate ins in clangd-context.ts
import * as testAst from './test-ast';
testAst.activate(this);
clangd code walkthrough
clangd source code
Clangd Getting started
vscode-clangd source code