[clang-tools-extra] [llvm] [Clangd] Add AST search capabilities from clang-query (PR #156090)
Fabian Keßler-Schulz via cfe-commits
cfe-commits at lists.llvm.org
Sat Aug 15 14:45:36 PDT 2026
https://github.com/Febbe updated https://github.com/llvm/llvm-project/pull/156090
>From 3d41a8cce0d25173c7ce9eb64d278c59b36e8615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Wed, 2 Oct 2024 16:33:22 +0200
Subject: [PATCH 1/4] Add AST Search Capabilities to clangd - utilizes
clang_query to find nodes in the AST - searches and returns the bound node
names - added astSearchProvider to capabilities - added tests
---
clang-tools-extra/clangd/CMakeLists.txt | 2 +
clang-tools-extra/clangd/ClangdLSPServer.cpp | 15 +++++
clang-tools-extra/clangd/ClangdLSPServer.h | 1 +
clang-tools-extra/clangd/ClangdServer.cpp | 36 +++++++++++
clang-tools-extra/clangd/ClangdServer.h | 2 +
clang-tools-extra/clangd/Protocol.cpp | 13 ++++
clang-tools-extra/clangd/Protocol.h | 14 +++++
clang-tools-extra/clangd/XRefs.cpp | 59 +++++++++++++++++++
clang-tools-extra/clangd/XRefs.h | 10 ++++
.../clangd/test/find-in-ast.test | 37 ++++++++++++
.../clangd/test/initialize-params.test | 4 ++
llvm/include/llvm/Support/JSON.h | 4 ++
12 files changed, 197 insertions(+)
create mode 100644 clang-tools-extra/clangd/test/find-in-ast.test
diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index 9fa7d92d8374c..cfdfd61a31329 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -60,6 +60,7 @@ endif()
include_directories(BEFORE "${CMAKE_CURRENT_BINARY_DIR}/../clang-tidy")
include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../include-cleaner/include")
+include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../clang-query")
add_clang_library(clangDaemon STATIC
AST.cpp
@@ -186,6 +187,7 @@ target_link_libraries(clangDaemon
${LLVM_PTHREAD_LIB}
clangIncludeCleaner
+ clangQuery
clangTidy
clangTidyUtils
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 43e8b35e45c89..5e0b474e47651 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -75,6 +75,7 @@ std::optional<int64_t> decodeVersion(llvm::StringRef Encoded) {
const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
+constexpr llvm::StringLiteral SearchASTMethod = "textDocument/searchAST";
CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
const URIForFile &File) {
@@ -637,6 +638,9 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
{"workspaceSymbolProvider", true},
{"referencesProvider", true},
{"astProvider", true}, // clangd extension
+ {"astSearchProvider",
+ llvm::json::Object{{"search", true},
+ {"replace", false}}}, // clangd extension
{"typeHierarchyProvider", true},
// Unfortunately our extension made use of the same capability name as the
// standard. Advertise this capability to tell clients that implement our
@@ -851,6 +855,16 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
});
}
+void ClangdLSPServer::onMethodSearchAST(const SearchASTArgs &Args,
+ Callback<llvm::json::Value> Reply) {
+ Server->findAST(Args, [Reply = std::move(Reply)](
+ llvm::Expected<BoundASTNodes> BoundNodes) mutable {
+ if (!BoundNodes)
+ return Reply(BoundNodes.takeError());
+ return Reply(*BoundNodes);
+ });
+}
+
void ClangdLSPServer::applyEdit(WorkspaceEdit WE, llvm::json::Value Success,
Callback<llvm::json::Value> Reply) {
ApplyWorkspaceEditParams Edit;
@@ -1733,6 +1747,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
+ Bind.method(SearchASTMethod, this, &ClangdLSPServer::onMethodSearchAST);
ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
PublishDiagnostics = Bind.outgoingNotification("textDocument/publishDiagnostics");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 6ada3fd9e6e47..8d7f4ccd67eea 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -186,6 +186,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
void onCommandApplyEdit(const WorkspaceEdit &, Callback<llvm::json::Value>);
void onCommandApplyTweak(const TweakArgs &, Callback<llvm::json::Value>);
void onCommandApplyRename(const RenameParams &, Callback<llvm::json::Value>);
+ void onMethodSearchAST(const SearchASTArgs &, Callback<llvm::json::Value>);
/// Outgoing LSP calls.
LSPBinder::OutgoingMethod<ApplyWorkspaceEditParams,
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 37eb82116f3a9..ebb2297d2b7c8 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -815,6 +815,42 @@ void ClangdServer::locateSymbolAt(PathRef File, Position Pos,
WorkScheduler->runWithAST("Definitions", File, std::move(Action));
}
+void ClangdServer::findAST(SearchASTArgs const &Args,
+ Callback<BoundASTNodes> CB) {
+ auto Action = [Args, CB = std::move(CB)](
+ llvm::Expected<InputsAndAST> InpAST) mutable {
+ if (!InpAST)
+ return CB(InpAST.takeError());
+ auto BoundNodes = clangd::locateASTQuery(InpAST->AST, Args);
+ if (!BoundNodes)
+ return CB(BoundNodes.takeError());
+ if (BoundNodes->empty())
+ return CB(error("No matching AST nodes found"));
+
+ auto &&AST = InpAST->AST;
+ // Convert BoundNodes to a vector of vectors to ASTNode's.
+ BoundASTNodes Result;
+ Result.reserve(BoundNodes->size());
+ for (auto &&BN : *BoundNodes) {
+ auto &&Map = BN.getMap();
+ BoundASTNodes::value_type BAN;
+ for (const auto &[Key, Value] : Map) {
+ BAN.emplace(Key, dumpAST(Value, AST.getTokens(), AST.getASTContext()));
+ }
+ if (BAN.empty())
+ continue;
+ Result.push_back(std::move(BAN));
+ }
+ if (Result.empty()) {
+ return CB(error("No AST nodes found for the query"));
+ }
+ CB(std::move(Result));
+ };
+
+ WorkScheduler->runWithAST("Definitions", Args.textDocument.uri.file(),
+ std::move(Action));
+}
+
void ClangdServer::switchSourceHeader(
PathRef Path, Callback<std::optional<clangd::Path>> CB) {
// We want to return the result as fast as possible, strategy is:
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 264ab7437c248..9a4ff39a2ad3e 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -268,6 +268,8 @@ class ClangdServer {
void locateSymbolAt(PathRef File, Position Pos,
Callback<std::vector<LocatedSymbol>> CB);
+ void findAST(const SearchASTArgs &Args, Callback<BoundASTNodes> CB);
+
/// Switch to a corresponding source file when given a header file, and vice
/// versa.
void switchSourceHeader(PathRef Path,
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index d5c50cc1cfa35..8b73529a94b88 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -13,6 +13,7 @@
#include "Protocol.h"
#include "URI.h"
#include "support/Logger.h"
+#include "clang/AST/ASTTypeTraits.h"
#include "clang/Basic/LLVM.h"
#include "clang/Index/IndexSymbol.h"
#include "llvm/ADT/StringExtras.h"
@@ -1674,6 +1675,18 @@ bool fromJSON(const llvm::json::Value &Params, SelectionRangeParams &S,
O.map("positions", S.positions);
}
+bool fromJSON(const llvm::json::Value &Params, SearchASTArgs &Args,
+ llvm::json::Path P) {
+ llvm::json::ObjectMapper O(Params, P);
+ return O && O.map("query", Args.searchQuery) &&
+ O.map("textDocument", Args.textDocument)
+ // && O.map("bindRoot", Args.bindRoot); TODO: add bindRoot to extend this
+ // feature
+ // && O.map("traversalKind", Args.tk); TODO: add traversalKind to extend
+ // this feature
+ ;
+}
+
llvm::json::Value toJSON(const SelectionRange &Out) {
if (Out.parent) {
return llvm::json::Object{{"range", Out.range},
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index b511ca495e47f..8cbab565828fc 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -26,6 +26,7 @@
#include "URI.h"
#include "index/SymbolID.h"
#include "support/MemoryTree.h"
+#include "clang/AST/ASTTypeTraits.h"
#include "clang/Index/IndexSymbol.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/JSON.h"
@@ -1507,6 +1508,19 @@ struct RenameParams {
bool fromJSON(const llvm::json::Value &, RenameParams &, llvm::json::Path);
llvm::json::Value toJSON(const RenameParams &);
+struct SearchASTArgs {
+ std::string searchQuery;
+ TextDocumentIdentifier textDocument;
+
+ // Todo (extend feature): make them members and modifiable:
+ /// wheter the whole query is shown
+ static auto constexpr BindRoot = true;
+ /// Simplify things for users; default for now.
+ static auto constexpr Tk = TraversalKind::TK_IgnoreUnlessSpelledInSource;
+};
+bool fromJSON(const llvm::json::Value &, SearchASTArgs &, llvm::json::Path);
+using BoundASTNodes = std::vector<std::map<std::string, struct ASTNode>>;
+
struct PrepareRenameResult {
/// Range of the string to rename.
Range range;
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 6886d235811d5..fd1b4af288979 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -14,6 +14,8 @@
#include "ParsedAST.h"
#include "Protocol.h"
#include "Quality.h"
+#include "Query.h"
+#include "QuerySession.h"
#include "Selection.h"
#include "SourceCode.h"
#include "clang-include-cleaner/Analysis.h"
@@ -41,6 +43,10 @@
#include "clang/AST/StmtCXX.h"
#include "clang/AST/StmtVisitor.h"
#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/ASTMatchers/Dynamic/Diagnostics.h"
+#include "clang/ASTMatchers/Dynamic/Parser.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
@@ -50,6 +56,7 @@
#include "clang/Index/IndexingAction.h"
#include "clang/Index/IndexingOptions.h"
#include "clang/Lex/Lexer.h"
+#include "clang/Parse/Parser.h"
#include "clang/Sema/HeuristicResolver.h"
#include "clang/Tooling/Syntax/Tokens.h"
#include "clang/UnifiedSymbolResolution/USRGeneration.h"
@@ -66,6 +73,7 @@
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
+#include <cmath>
#include <optional>
#include <string>
#include <vector>
@@ -822,6 +830,57 @@ const syntax::Token *findNearbyIdentifier(const SpelledWord &Word,
return BestTok;
}
+auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &Query)
+ -> llvm::Expected<std::vector<ast_matchers::BoundNodes>> {
+ using namespace ast_matchers;
+ using namespace ast_matchers::dynamic;
+ using ast_matchers::dynamic::Parser;
+
+ Diagnostics Diag;
+ auto MatcherSource = llvm::StringRef(Query.searchQuery).ltrim();
+
+ std::optional<DynTypedMatcher> Matcher = Parser::parseMatcherExpression(
+ MatcherSource,
+ nullptr /* is this sema instance usefull, to reduce overhead?*/,
+ nullptr /*we currently don't support let*/, &Diag);
+ if (!Matcher) {
+ return error("Not a valid top-level matcher: {}.", Diag.toString());
+ }
+
+ struct CollectBoundNodes : MatchFinder::MatchCallback {
+ std::vector<BoundNodes> *Bindings;
+ CollectBoundNodes(std::vector<BoundNodes> &Bindings)
+ : Bindings(&Bindings) {}
+ void run(const MatchFinder::MatchResult &Result) override {
+ Bindings->push_back(Result.Nodes);
+ }
+ };
+
+ DynTypedMatcher MaybeBoundMatcher = *Matcher;
+ if (Query.BindRoot) {
+ std::optional<DynTypedMatcher> M = Matcher->tryBind("root");
+ if (M)
+ MaybeBoundMatcher = *M;
+ }
+ std::vector<BoundNodes> Matches;
+ CollectBoundNodes Collect(Matches);
+
+ MatchFinder::MatchFinderOptions Opt;
+ Opt.IgnoreSystemHeaders = true;
+ MatchFinder Finder{Opt};
+ if (!Finder.addDynamicMatcher(MaybeBoundMatcher, &Collect)) {
+ return error("Can't add matcher.");
+ }
+
+ ASTContext &Ctx = AST.getASTContext();
+
+ auto OldTK = Ctx.getParentMapContext().getTraversalKind();
+ Ctx.getParentMapContext().setTraversalKind(Query.Tk);
+ Finder.matchAST(Ctx);
+ Ctx.getParentMapContext().setTraversalKind(OldTK);
+ return Matches;
+}
+
std::vector<LocatedSymbol> locateSymbolAt(ParsedAST &AST, Position Pos,
const SymbolIndex *Index) {
const auto &SM = AST.getSourceManager();
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 247e52314c3f9..d6c0e3b99941b 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -19,6 +19,7 @@
#include "index/SymbolID.h"
#include "support/Path.h"
#include "clang/AST/ASTTypeTraits.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>
@@ -32,6 +33,15 @@ class TokenBuffer;
namespace clangd {
class ParsedAST;
+struct LocatedAST {
+ ast_matchers::BoundNodes &AST;
+};
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &, const LocatedAST &);
+
+auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &)
+ -> llvm::Expected<std::vector<ast_matchers::BoundNodes>>;
+
// Describes where a symbol is declared and defined (as far as clangd knows).
// There are three cases:
// - a declaration only, no definition is known (e.g. only header seen)
diff --git a/clang-tools-extra/clangd/test/find-in-ast.test b/clang-tools-extra/clangd/test/find-in-ast.test
new file mode 100644
index 0000000000000..6031ad10de1b3
--- /dev/null
+++ b/clang-tools-extra/clangd/test/find-in-ast.test
@@ -0,0 +1,37 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace --dump-input always %s
+void bob();
+void f() {
+ bob();
+}
+---
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"foldingRange": {"lineFoldingOnly": true}}},"trace":"off"}}
+---
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"cpp","text":"void bob();\nvoid f() {\n bob();\n}\n","uri":"test:///foo.cpp","version":1}}}
+---
+{"id":1,"jsonrpc":"2.0","method":"textDocument/searchAST","params":{"textDocument":{"uri":"test:///foo.cpp"},"query":"declRefExpr(to(namedDecl(hasName(\"bob\"))))"}}
+# CHECK: "id": 1,
+# CHECK-NEXT: "jsonrpc": "2.0",
+# CHECK-NEXT: "result": [
+# CHECK-NEXT: {
+# CHECK-NEXT: "root": {
+# CHECK-NEXT: "arcana": "DeclRefExpr {{.*}} 'void ()' lvalue Function {{.*}} 'bob' 'void ()'",
+# CHECK-NEXT: "detail": "bob",
+# CHECK-NEXT: "kind": "DeclRef",
+# CHECK-NEXT: "range": {
+# CHECK-NEXT: "end": {
+# CHECK-NEXT: "character": 5,
+# CHECK-NEXT: "line": 2
+# CHECK-NEXT: },
+# CHECK-NEXT: "start": {
+# CHECK-NEXT: "character": 2,
+# CHECK-NEXT: "line": 2
+# CHECK-NEXT: }
+# CHECK-NEXT: },
+# CHECK-NEXT: "role": "expression"
+# CHECK-NEXT: }
+# CHECK-NEXT: }
+# CHECK-NEXT: ]
+---
+{"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
diff --git a/clang-tools-extra/clangd/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test
index d976b7d19fd0e..54d5b64ff2972 100644
--- a/clang-tools-extra/clangd/test/initialize-params.test
+++ b/clang-tools-extra/clangd/test/initialize-params.test
@@ -6,6 +6,10 @@
# CHECK-NEXT: "result": {
# CHECK-NEXT: "capabilities": {
# CHECK-NEXT: "astProvider": true,
+# CHECK-NEXT: "astSearchProvider": {
+# CHECK-NEXT: "replace": false,
+# CHECK-NEXT: "search": true
+# CHECK-NEXT: },
# CHECK-NEXT: "callHierarchyProvider": true,
# CHECK-NEXT: "clangdInlayHintsProvider": true,
# CHECK-NEXT: "codeActionProvider": true,
diff --git a/llvm/include/llvm/Support/JSON.h b/llvm/include/llvm/Support/JSON.h
index 3b145a69c74cb..74066e4efcd3c 100644
--- a/llvm/include/llvm/Support/JSON.h
+++ b/llvm/include/llvm/Support/JSON.h
@@ -111,6 +111,10 @@ class Object {
// (using std::pair forces extra copies).
struct KV;
explicit Object(std::initializer_list<KV> Properties);
+ template <typename Collection> explicit Object(Collection &&C) {
+ for (auto &&P : C)
+ M.insert(P);
+ }
iterator begin() { return M.begin(); }
const_iterator begin() const { return M.begin(); }
>From 7ab24e572d819fa5cd94cd43b7dde618eb4b9e87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Sat, 15 Aug 2026 22:38:48 +0200
Subject: [PATCH 2/4] Add capabilities to complete a matcher expression
- adds a clang/completeASTMatcher method to the
clangd LSP server, which returns a list of all
available AST matcher expressions and the text
to complete the current expression.
- adds a test for the new method, which checks that
the returned completions return the completion.
- uses already existing code from ast-matchers.
- mirrors behaviour of clang-query
---
clang-tools-extra/clangd/ClangdLSPServer.cpp | 15 ++++++++++++
clang-tools-extra/clangd/ClangdLSPServer.h | 2 ++
clang-tools-extra/clangd/ClangdServer.cpp | 21 +++++++++++++++++
clang-tools-extra/clangd/ClangdServer.h | 3 +++
clang-tools-extra/clangd/Protocol.cpp | 19 +++++++++++++++
clang-tools-extra/clangd/Protocol.h | 19 +++++++++++++++
.../clangd/test/completion-matcher.test | 23 +++++++++++++++++++
7 files changed, 102 insertions(+)
create mode 100644 clang-tools-extra/clangd/test/completion-matcher.test
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 5e0b474e47651..ce9e264f9bd9b 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -76,6 +76,8 @@ const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
constexpr llvm::StringLiteral SearchASTMethod = "textDocument/searchAST";
+constexpr llvm::StringLiteral CompleteASTMatcherMethod =
+ "clangd/completeASTMatcher";
CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
const URIForFile &File) {
@@ -855,6 +857,18 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
});
}
+void ClangdLSPServer::onMethodCompleteASTMatcher(
+ const CompleteASTMatcherArgs &Args, Callback<llvm::json::Value> Reply) {
+ Server->completeASTMatcher(
+ Args,
+ [Reply = std::move(Reply)](
+ llvm::Expected<std::vector<ASTMatcherCompletion>> ToolTips) mutable {
+ if (!ToolTips)
+ return Reply(ToolTips.takeError());
+ return Reply(*ToolTips);
+ });
+}
+
void ClangdLSPServer::onMethodSearchAST(const SearchASTArgs &Args,
Callback<llvm::json::Value> Reply) {
Server->findAST(Args, [Reply = std::move(Reply)](
@@ -1747,6 +1761,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
+ Bind.method(CompleteASTMatcherMethod, this, &ClangdLSPServer::onMethodCompleteASTMatcher);
Bind.method(SearchASTMethod, this, &ClangdLSPServer::onMethodSearchAST);
ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 8d7f4ccd67eea..07a4b16a71e07 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -186,6 +186,8 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
void onCommandApplyEdit(const WorkspaceEdit &, Callback<llvm::json::Value>);
void onCommandApplyTweak(const TweakArgs &, Callback<llvm::json::Value>);
void onCommandApplyRename(const RenameParams &, Callback<llvm::json::Value>);
+ void onMethodCompleteASTMatcher(const CompleteASTMatcherArgs &,
+ Callback<llvm::json::Value>);
void onMethodSearchAST(const SearchASTArgs &, Callback<llvm::json::Value>);
/// Outgoing LSP calls.
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index ebb2297d2b7c8..fb4592bd57db8 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -35,6 +35,8 @@
#include "support/MemoryTree.h"
#include "support/ThreadsafeFS.h"
#include "support/Trace.h"
+#include "clang/ASTMatchers/Dynamic/Parser.h"
+#include "clang/ASTMatchers/Dynamic/Registry.h"
#include "clang/Basic/Stack.h"
#include "clang/Format/Format.h"
#include "clang/Lex/Preprocessor.h"
@@ -815,6 +817,25 @@ void ClangdServer::locateSymbolAt(PathRef File, Position Pos,
WorkScheduler->runWithAST("Definitions", File, std::move(Action));
}
+void ClangdServer::completeASTMatcher(
+ const CompleteASTMatcherArgs &Args,
+ Callback<std::vector<ASTMatcherCompletion>> CB) {
+ auto Action = [Args, CB = std::move(CB)]() mutable -> void {
+ using ::clang::ast_matchers::dynamic::Parser;
+ using ::clang::ast_matchers::dynamic::MatcherCompletion;
+
+ std::vector<ASTMatcherCompletion> ToolTips;
+ auto QueryRef = StringRef(Args.searchQuery);
+ std::vector<MatcherCompletion> Comps =
+ Parser::completeExpression(QueryRef, Args.offset);
+ for (auto I = Comps.begin(), E = Comps.end(); I != E; ++I) {
+ ToolTips.push_back(ASTMatcherCompletion{I->TypedText, I->MatcherDecl});
+ }
+ return CB(std::move(ToolTips));
+ };
+ WorkScheduler->runQuick("ASTMatcherCompletion", "", std::move(Action));
+}
+
void ClangdServer::findAST(SearchASTArgs const &Args,
Callback<BoundASTNodes> CB) {
auto Action = [Args, CB = std::move(CB)](
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 9a4ff39a2ad3e..73241b0c055cd 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -270,6 +270,9 @@ class ClangdServer {
void findAST(const SearchASTArgs &Args, Callback<BoundASTNodes> CB);
+ void completeASTMatcher(const CompleteASTMatcherArgs &Args,
+ Callback<std::vector<ASTMatcherCompletion>> CB);
+
/// Switch to a corresponding source file when given a header file, and vice
/// versa.
void switchSourceHeader(PathRef Path,
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 8b73529a94b88..f65807ca8f3bd 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -1675,6 +1675,13 @@ bool fromJSON(const llvm::json::Value &Params, SelectionRangeParams &S,
O.map("positions", S.positions);
}
+bool fromJSON(const llvm::json::Value &Params, CompleteASTMatcherArgs &Args,
+ llvm::json::Path P) {
+ llvm::json::ObjectMapper O(Params, P);
+ return O && O.map("searchQuery", Args.searchQuery) &&
+ O.map("offset", Args.offset);
+}
+
bool fromJSON(const llvm::json::Value &Params, SearchASTArgs &Args,
llvm::json::Path P) {
llvm::json::ObjectMapper O(Params, P);
@@ -1782,6 +1789,18 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const ASTNode &Root) {
return OS;
}
+llvm::json::Value toJSON(const ASTMatcherCompletion &ASTTT) {
+ llvm::json::Object Result{{"typedText", ASTTT.typedText},
+ {"matcherDecl", ASTTT.matcherDecl}};
+ return Result;
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+ const ASTMatcherCompletion &ASTTT) {
+ OS << ASTTT.typedText << " - " << ASTTT.matcherDecl;
+ return OS;
+}
+
bool fromJSON(const llvm::json::Value &E, SymbolID &S, llvm::json::Path P) {
auto Str = E.getAsString();
if (!Str) {
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 8cbab565828fc..f710246e45a7f 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1508,6 +1508,13 @@ struct RenameParams {
bool fromJSON(const llvm::json::Value &, RenameParams &, llvm::json::Path);
llvm::json::Value toJSON(const RenameParams &);
+struct CompleteASTMatcherArgs {
+ std::string searchQuery;
+ size_t offset;
+};
+bool fromJSON(const llvm::json::Value &, CompleteASTMatcherArgs &,
+ llvm::json::Path);
+
struct SearchASTArgs {
std::string searchQuery;
TextDocumentIdentifier textDocument;
@@ -2115,6 +2122,18 @@ struct ASTNode {
llvm::json::Value toJSON(const ASTNode &);
llvm::raw_ostream &operator<<(llvm::raw_ostream &, const ASTNode &);
+/// The query completion tooltip.
+struct ASTMatcherCompletion {
+ /// Text to complete the matcher.
+ std::string typedText;
+ /// Full Matcher declaration including type and parameters
+ std::string matcherDecl;
+};
+
+llvm::json::Value toJSON(const ASTMatcherCompletion &);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &,
+ const ASTMatcherCompletion &);
+
} // namespace clangd
} // namespace clang
diff --git a/clang-tools-extra/clangd/test/completion-matcher.test b/clang-tools-extra/clangd/test/completion-matcher.test
new file mode 100644
index 0000000000000..1ff751ef33a4b
--- /dev/null
+++ b/clang-tools-extra/clangd/test/completion-matcher.test
@@ -0,0 +1,23 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace --dump-input always %s
+void bob();
+void f() {
+ bob();
+}
+---
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"foldingRange": {"lineFoldingOnly": true}}},"trace":"off"}}
+---
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"cpp","text":"void bob();\nvoid f() {\n bob();\n}\n","uri":"test:///foo.cpp","version":1}}}
+---
+{"id":1,"jsonrpc":"2.0","method":"clangd/completeASTMatcher","params":{"query":"declRefE", "offset": 8}}
+# CHECK: "id": 1,
+# CHECK-NEXT: "jsonrpc": "2.0",
+# CHECK-NEXT: "result": [
+# CHECK-NEXT: {
+# CHECK-NEXT: "matcherDecl": "Matcher<Stmt> declRefExpr(Matcher<DeclRefExpr>...)",
+# CHECK-NEXT: "typedText": "xpr("
+# CHECK-NEXT: }
+# CHECK-NEXT: ]
+---
+{"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}
>From cbc1e12b3c56ffab08ea0533a76c188c4d77515a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Sat, 15 Aug 2026 23:44:48 +0200
Subject: [PATCH 3/4] fixes for first commit
---
clang-tools-extra/clangd/ClangdServer.cpp | 2 +-
clang-tools-extra/clangd/XRefs.cpp | 3 ---
clang-tools-extra/clangd/XRefs.h | 6 ------
3 files changed, 1 insertion(+), 10 deletions(-)
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index fb4592bd57db8..4817e28d0f75c 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -868,7 +868,7 @@ void ClangdServer::findAST(SearchASTArgs const &Args,
CB(std::move(Result));
};
- WorkScheduler->runWithAST("Definitions", Args.textDocument.uri.file(),
+ WorkScheduler->runWithAST("FindAST", Args.textDocument.uri.file(),
std::move(Action));
}
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index fd1b4af288979..6c9fb53dd2722 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -14,8 +14,6 @@
#include "ParsedAST.h"
#include "Protocol.h"
#include "Quality.h"
-#include "Query.h"
-#include "QuerySession.h"
#include "Selection.h"
#include "SourceCode.h"
#include "clang-include-cleaner/Analysis.h"
@@ -73,7 +71,6 @@
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
-#include <cmath>
#include <optional>
#include <string>
#include <vector>
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index d6c0e3b99941b..63e1a72093459 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -33,12 +33,6 @@ class TokenBuffer;
namespace clangd {
class ParsedAST;
-struct LocatedAST {
- ast_matchers::BoundNodes &AST;
-};
-
-llvm::raw_ostream &operator<<(llvm::raw_ostream &, const LocatedAST &);
-
auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &)
-> llvm::Expected<std::vector<ast_matchers::BoundNodes>>;
>From dae08a52bddc901a5c0544fede790d4898698134 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Sat, 15 Aug 2026 23:44:59 +0200
Subject: [PATCH 4/4] fix for second
---
clang-tools-extra/clangd/test/completion-matcher.test | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/clangd/test/completion-matcher.test b/clang-tools-extra/clangd/test/completion-matcher.test
index 1ff751ef33a4b..009352538016b 100644
--- a/clang-tools-extra/clangd/test/completion-matcher.test
+++ b/clang-tools-extra/clangd/test/completion-matcher.test
@@ -8,7 +8,7 @@ void f() {
---
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"cpp","text":"void bob();\nvoid f() {\n bob();\n}\n","uri":"test:///foo.cpp","version":1}}}
---
-{"id":1,"jsonrpc":"2.0","method":"clangd/completeASTMatcher","params":{"query":"declRefE", "offset": 8}}
+{"id":1,"jsonrpc":"2.0","method":"clangd/completeASTMatcher","params":{"searchQuery":"declRefE", "offset": 8}}
# CHECK: "id": 1,
# CHECK-NEXT: "jsonrpc": "2.0",
# CHECK-NEXT: "result": [
More information about the cfe-commits
mailing list