... is a simple C++ and CMake wrapper around tree-sitter. This project provides CMake definitions and a C++ wrapper that help with
- managing tree-sitter and tree-sitter grammars as dependencies
- accessing basic tree-sitter APIs for parse tree inspection
There is also opt-in support for smoothing out C++ interactions with
- formatters for tree-sitter wrapper types
- queries
- visitor and fold operations over tree-sitter trees
... for version 0.1.0+ include
- C++23 (gcc 14+, clang 18+ with libstdc++) (for
std::expected,std::print, ...) - CMake 3.28+
Tag v0.0.3 should still support older versions of C++ and CMake.
... requires the CPM CMake module
for fetching and managing dependencies from github. Adding cpp-tree-sitter
as a CPM dependency makes cpp-tree-sitter available as a library and
provides a function, add_grammar_from_repo, that will download and
make available a standard tree-sitter grammar on GitHub as a library.
The tree-sitter parser
example
can be reproduced in a CMake project with CPM by including the following in
CMakeLists.txt:
include(cmake/CPM.cmake)
# Downloads this wrapper library and tree-sitter.
# Makes them available via the `cpp-tree-sitter` CMake library target.
CPMAddPackage(
NAME cpp-tree-sitter
GIT_REPOSITORY https://github.com/nsumner/cpp-tree-sitter.git
GIT_TAG v0.1.0
)
# Downloads a tree-sitter grammar from github and makes it available as a
# cmake library target.
add_grammar_from_repo(
NAME tree-sitter-json # Defines the library name for a grammar
REPO https://github.com/tree-sitter/tree-sitter-json.git # Repository URL of a tree-sitter grammar
VERSION 0.21.0 # Version tag for the grammar
)
# Use the library in a demo program.
add_executable(demo)
target_sources(demo
PRIVATE
demo.cpp
)
target_link_libraries(demo
PRIVATE
tree-sitter-json
cts::cpp-tree-sitter
)Translating the parsing and tree inspection operations from the example to
use the C++ wrappers then yields a demo.cpp like:
#include <cstdlib>
#include <print>
#include <string_view>
#include <cpp-tree-sitter.h>
#include <cts/format.h>
extern "C" TSLanguage* tree_sitter_json();
int
main() {
auto parser = ts::Parser::create(tree_sitter_json());
if (!parser) {
std::println(stderr, "error: {}", parser.error());
return EXIT_FAILURE;
}
constexpr std::string_view sourcecode = "[1, null]";
const auto tree = parser->parse(sourcecode);
if (!tree) {
std::println(stderr, "error: {}", tree.error());
return EXIT_FAILURE;
}
const ts::Node root = tree->getRootNode();
std::println("root: {}", root.getType());
for (const ts::Node child : root.getNamedChild(0)->getNamedChildren()) {
std::println(" {}: '{}'", child, child.getSourceRange(sourcecode));
}
std::println("Syntax tree: {}", root.getSExpr());
return EXIT_SUCCESS;
}In particular, some of the underlying APIs now use method calls for easier discoverability, and resource cleaning is automatic.
Core functionality can be included with just <cpp-tree-sitter.h>, but extra
features that might be more expensive are opt-in via:
<cts/format.h>- modern C++ formatters<cts/query.h>- query processing<cts/typed.h>- a type-dispatching visitor<cts/loader.h>- dynamically loading grammar plugins
... can be found in the examples/ directory. Each one builds
and runs, and each carries details and explanation in comments at the top of
the file.
parse_demo.cpp— basic parsing and tree inspectionquery_demo.cpp— running a query, and how predicates are evaluatedquery_advanced_demo.cpp— restricting, limiting, cancelling, and narrowing a queryvisitor_demo.cpp— walking a tree withts::visitandts::preorderfold_demo.cpp— computing a value withts::foldtyped_demo.cpp— dispatching on node type withts::typededit_demo.cpp— incremental parsing after an editload_demo.cpp— loading a grammar an editor already installed
examples/CMakeLists.txt shows how to wire them up
against cts::cpp-tree-sitter and a grammar fetched from github or against
cts::cpp-tree-sitter-dynamic and a dynamically loaded grammar plugin.
Longer explanations that do not fit in an example live in docs/:
build and install configurations,
lifetimes and types,
formatting, and
loading grammars at run time.
... can be done with Node::getChildren() and Node::getNamedChildren()
ranges. They return a ChildrenView, a lazy std::ranges::view over a
node's children:
for (ts::Node child : root.getNamedChild(0)->getNamedChildren()) {
std::println(" {}: '{}'", child.getType(), child.getSourceRange(sourcecode));
}
auto newList = node.getChildren()
| std::views::filter(...)
| std::views::transform(...)
| std::ranges::to<std::vector>();A ChildrenView holds its parent Node by value, so it is copyable and
behaves intuitively.
... are managed through API types. Operations that can fail return
std::expected<T, ts::Error>. Questions about whether something exists
return std::optional<T>.
auto parser = ts::Parser::create(tree_sitter_json());
if (!parser) {
std::println(stderr, "error: {}", parser.error());
return EXIT_FAILURE;
}Tree and Parser own their tree-sitter resources and free them when
destroyed. Node borrows from a Tree. Keep the owning Tree alive for
as long as you hold Nodes from it.
docs/lifetimes-and-types.md lists which
operations can fail, why, borrowing details.
... operations help with traversing and computing values from a parse tree.
ts::visit walks a subtree with a visitor object. Any object with onEnter()
can be used as a visitor.
struct CountNamed {
int count = 0;
ts::VisitAction onEnter(ts::Node node) {
count += node.isNamed() ? 1 : 0;
return ts::VisitAction::Continue; // or SkipChildren, or Stop
}
};
CountNamed counter;
ts::visit(root, counter);An optional onLeave(ts::Node) fires on the way back out. visit is
cursor-based, so it neither recurses nor allocates. For the same walk as a lazy
range, ts::preorder composes with <ranges>:
auto numbers = ts::preorder(root)
| std::views::filter([](ts::Node n) { return n.getType() == "number"; });ts::fold computes a value bottom-up instead, building each node's result from
its children's results:
struct MaxDepth {
int onNode(ts::Node node, std::span<int> children) {
int deepest = 0;
for (int depth : children) { deepest = std::max(deepest, depth); }
return node.isNamed() ? deepest + 1 : deepest;
}
};
MaxDepth folder;
int depth = ts::fold<int>(root, folder);The fold result type is explicit at the call site. Leaves in the tree receive
an empty children span. Unlike visit, fold allocates to keep a stack of
child-result frames.
See examples/visitor_demo.cpp and
examples/fold_demo.cpp.
... with ts::typed enable dispatching to different methods based on the
type of the node being visited.
#include <cts/typed.h>
auto visitor = ts::typed(language,
ts::on<"number">([&](ts::Node node) { numbers.push_back(node); }),
ts::on<"comment", "string">([](ts::Node) {
return ts::VisitAction::SkipChildren; // one handler, several types
}),
ts::otherwise([](ts::Node) { /* everything else, punctuation included */ }));
if (!visitor) { /* error.name is the handler or node type at fault */ }
ts::visit(root, *visitor); // an ordinary TreeVisitorHandler names are checked against the language when the visitor is built, so
mistyped grammar labels fail up front. Supertypes are supported, and matching
uses a lookup table that is most-specific-first. A visitor first tries the
exact type, then the nearest supertype like _expression, then otherwise.
ts::typedFold<R> combines that dispatch with fold-style value production.
Every handler returns an R, and child results are pulled on demand:
auto sum = ts::typedFold<int>(language,
ts::on<"number">([&](ts::Node node) { return toInt(node); }),
ts::otherwise([](ts::Node node, ts::WalkFold<int>& walk) {
int total = 0;
for (ts::Node child : node.getNamedChildren()) { total += walk(child); }
return total;
}));
int result = sum->run(root);A handler can also take a ts::Walk& and drive the traversal itself.
examples/typed_demo.cpp shows more.
... is opted into with <cts/format.h> and provides expected formatting for
wrapper types.
#include <cts/format.h> // core types
#include <cts/query/format.h> // query types (extra opt-in for queries){} on a ts::Node renders as number [1, 2) and allocates nothing, so it is
safe to use inside a visitor. docs/formatting.md has
the full table of what renders as what.
... is opted-into through <cts/query.h>. ts::Query::create compiles one
Query against a language, and a ts::QueryCursor runs it:
#include <cts/query.h>
auto query = ts::Query::create(
tree_sitter_json(),
"(pair key: (string) @key value: (_) @value)");
if (!query) { /* handle error */ }
// Resolve capture names to ids once ahead of time.
auto key = query->getCaptureId("key");
ts::QueryCursor cursor;
auto matches = cursor.getMatches(*query, root);
if (!matches) { /* wrong language for this tree, or bad options */ }
for (const ts::QueryMatch& match : *matches) {
if (auto node = match.getNodeFor(*key)) {
std::println("key: '{}'", node->getSourceRange(source));
}
}Three rules cover most uses:
- A match and its captures are valid only until the cursor advances. Copy out whatever you want to keep.
- Text predicates like
#eq?and#match?are evaluated for you but need the source text. Pass{.source = source}. A#match?also needs a regex engine, which you can choose yourself and supply atcreate. - One cursor runs one query at a time. Use a separate cursor for each concurrent iteration.
A query over a large tree can be bounded with a progress callback, which
tree-sitter invokes periodically during execution. Returning
ts::ProgressAction::Cancel ends the execution with the results so far.
auto matches = cursor.getMatches(*query, root, {
.onProgress = [deadline](uint32_t /*byteOffset*/) {
return std::chrono::steady_clock::now() > deadline
? ts::ProgressAction::Cancel
: ts::ProgressAction::Continue;
}});
for (const ts::QueryMatch& match : *matches) { /* ... */ }
if (cursor.wasCancelled()) { /* ... */ }examples/query_demo.cpp covers predicates.
More advanced options are shown in
examples/query_advanced_demo.cpp.
... depends on CPM as shown at the top of this README.
docs/cmake.md covers alternative use cases like
- building a grammar you already have on disk, with
add_grammar_from_path - repositories holding several grammars, with
SUBDIRECTORY - installing the library once and consuming it with
find_package - linking against a system tree-sitter, with
-DCTS_SYSTEM_TREE_SITTER=ON - providing your own
tree-sittertarget - building a grammar as a loadable module instead, with
add_grammar_module
... allows you to reuse grammar plugins that you might have installed with tree-sitter or that might be used with an IDE.
#include <cts/loader.h>
ts::GrammarSearchPath const grammars{ts::layout::nvimTreesitter()};
auto language = grammars.load("json");
if (!language) { /* the error names every directory that was searched */ }Support is opt-in and POSIX-only behind a second CMake target,
cts::cpp-tree-sitter-dynamic. A loaded grammar is never unloaded, so the
ts::Language stays valid for the lifetime of the process.
docs/dynamic-grammars.md shows APIs that can
help to find and load a particular grammar, and
examples/load_demo.cpp shows them in action.