This commit is contained in:
Grace Yoder 2026-07-15 15:19:45 -06:00
commit 6a3f7b3fde
Signed by: grace
SSH key fingerprint: SHA256:oG9v0ZlohsoMAg083HAwBGOcaPLF3nIuOlAW4MtASMo
7 changed files with 399 additions and 0 deletions

19
.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
# This file is for zig-specific build artifacts.
# If you have OS-specific or editor-specific files to ignore,
# such as *.swp or .DS_Store, put those in your global
# ~/.gitignore and put this in your ~/.gitconfig:
#
# [core]
# excludesfile = ~/.gitignore
#
# Cheers!
# -andrewrk
.zig-cache/
zig-out/
zig-pkg/
/release/
/debug/
/build/
/build-*/
/docgen_tmp/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright 2026 Grace Yoder
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
README.md Normal file
View file

@ -0,0 +1,6 @@
# EZTS
## Easy Zig Tree Sitter
Managed Grammars and Highlight Queries for use with [Tree-Sitter Zig Bindings](https://github.com/tree-sitter/zig-tree-sitter)

224
build.zig Normal file
View file

@ -0,0 +1,224 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("eazy_zig_tree_sitter", .{
.root_source_file = b.path("src/root.zig"),
});
const exe = b.addExecutable(.{
.name = "eazy_zig_tree_sitter",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "eazy_zig_tree_sitter", .module = mod },
},
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const tree_sitter = b.dependency("tree_sitter", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("tree-sitter", tree_sitter.module("tree_sitter"));
mod.addImport("tree-sitter", tree_sitter.module("tree_sitter"));
const use_all_langs = b.option(bool, "use_all_langs", "Build and Include all language grammars") orelse true;
const use_langs = b.option([]const []const u8, "use_langs", "List of langs to build (ie. `md`, `c`, `zig`)") orelse &[_][]const u8{};
var gb = GrammarBuilder{
.b = b,
.mod = mod,
.target = target,
.optimize = optimize,
.use_all_langs = use_all_langs,
.use_langs = use_langs,
};
// ========================
// Add Grammar Dependencies
// ========================
const ts_c = b.dependency("tree_sitter_c", .{});
gb.addGrammarChecked(ts_c, "", "c", "tree_sitter_c", false, "queries/highlights.scm");
const ts_md = b.dependency("tree_sitter_markdown", .{});
gb.addGrammarChecked(ts_md, "tree-sitter-markdown", "md", "tree_sitter_markdown", true, "queries/highlights.scm");
gb.addGrammarChecked(ts_md, "tree-sitter-markdown-inline", "md_inline", "tree_sitter_markdown_inline", true, "queries/highlights.scm");
const ts_lua = b.dependency("tree_sitter_lua", .{});
gb.addGrammarChecked(ts_lua, "", "lua", "tree_sitter_lua", true, "queries/highlights.scm");
mod.addImport("grammars", gb.buildGrammarsModule());
mod.addImport("queries", gb.buildQueriesModule());
}
const GrammarEntry = struct {
key: []const u8,
symbol: []const u8,
};
const QueryEntry = struct {
key: []const u8,
module: *std.Build.Module,
};
const GrammarBuilder = struct {
b: *std.Build,
mod: *std.Build.Module,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
grammars: std.ArrayList(GrammarEntry) = .empty,
queries: std.ArrayList(QueryEntry) = .empty,
grammars_module: ?*std.Build.Module = null,
queries_module: ?*std.Build.Module = null,
use_all_langs: bool,
use_langs: []const []const u8,
fn addGrammarChecked(
self: *GrammarBuilder,
dep: *std.Build.Dependency,
subdir: []const u8,
key: []const u8,
symbol: []const u8,
has_scanner: bool,
embed_query: ?[]const u8,
) void {
if (self.use_all_langs or
for (self.use_langs) |lang| {
if (std.mem.eql(u8, lang, key)) break true;
} else false)
{
self.addGrammar(
dep,
subdir,
key,
symbol,
has_scanner,
embed_query,
);
}
}
fn addGrammar(
self: *GrammarBuilder,
dep: *std.Build.Dependency,
subdir: []const u8,
key: []const u8,
symbol: []const u8,
has_scanner: bool,
embed_query: ?[]const u8,
) void {
const b = self.b;
const src_dir = if (subdir.len == 0) dep.path("src") else dep.path(b.pathJoin(&.{ subdir, "src" }));
const lib = b.addLibrary(.{
.name = symbol,
.linkage = .static,
.root_module = b.createModule(.{
.target = self.target,
.optimize = self.optimize,
.link_libc = true,
}),
});
const files: []const []const u8 = if (has_scanner) &.{ "parser.c", "scanner.c" } else &.{"parser.c"};
lib.root_module.addCSourceFiles(.{
.root = src_dir,
.files = files,
.flags = &.{"-std=c11"},
});
lib.root_module.addIncludePath(src_dir);
self.mod.linkLibrary(lib);
if (embed_query) |query_rel_path| {
const full_query_path = if (subdir.len == 0) dep.path(query_rel_path) else dep.path(b.pathJoin(&.{ subdir, query_rel_path }));
const query_mod = b.createModule(.{ .root_source_file = full_query_path });
self.queries.append(b.allocator, .{ .key = key, .module = query_mod }) catch @panic("OOM");
}
self.grammars.append(b.allocator, .{ .key = key, .symbol = symbol }) catch @panic("OOM");
}
/// makes the `grammar` submodule
fn buildGrammarsModule(self: *GrammarBuilder) *std.Build.Module {
if (self.grammars_module) |m| return m;
const b = self.b;
const tree_sitter = b.dependency("tree_sitter", .{ .target = self.target, .optimize = self.optimize });
var src: std.ArrayList(u8) = .empty;
src.appendSlice(b.allocator, "const std = @import(\"std\");\nconst ts = @import(\"tree-sitter\");\n\n") catch @panic("OOM");
for (self.grammars.items) |g| {
src.appendSlice(b.allocator, b.fmt(
"pub extern fn {s}() callconv(.c) *ts.Language;\n",
.{g.symbol},
)) catch @panic("OOM");
}
src.appendSlice(b.allocator, "\npub const languages = std.StaticStringMap(*const fn () callconv(.c) *ts.Language).initComptime(.{\n") catch @panic("OOM");
for (self.grammars.items) |g| {
src.appendSlice(b.allocator, b.fmt(
" .{{ \"{s}\", {s} }},\n",
.{ g.key, g.symbol },
)) catch @panic("OOM");
}
src.appendSlice(b.allocator, "});\n") catch @panic("OOM");
const wf = b.addWriteFiles();
self.grammars_module = b.createModule(.{
.root_source_file = wf.add("grammars.zig", src.items),
.imports = &.{
.{ .name = "tree-sitter", .module = tree_sitter.module("tree_sitter") },
},
});
return self.grammars_module.?;
}
/// makes the `queries` sub module
fn buildQueriesModule(self: *GrammarBuilder) *std.Build.Module {
if (self.queries_module) |m| return m;
const b = self.b;
var src: std.ArrayList(u8) = .empty;
for (self.queries.items) |q| {
src.appendSlice(b.allocator, b.fmt(
"pub const {s}_query = @embedFile(\"{s}_query\");\n",
.{ q.key, q.key },
)) catch @panic("OOM");
}
src.appendSlice(b.allocator, "\npub const queries = @import(\"std\").StaticStringMap([]const u8).initComptime(.{\n") catch @panic("OOM");
for (self.queries.items) |q| {
src.appendSlice(b.allocator, b.fmt(
" .{{ \"{s}\", {s}_query }},\n",
.{ q.key, q.key },
)) catch @panic("OOM");
}
src.appendSlice(b.allocator, "});\n") catch @panic("OOM");
var imports: std.ArrayList(std.Build.Module.Import) = .empty;
for (self.queries.items) |q| {
imports.append(b.allocator, .{ .name = b.fmt("{s}_query", .{q.key}), .module = q.module }) catch @panic("OOM");
}
const wf = b.addWriteFiles();
self.queries_module = b.createModule(.{
.root_source_file = wf.add("queries.zig", src.items),
.imports = imports.items,
});
return self.queries_module.?;
}
};

31
build.zig.zon Normal file
View file

@ -0,0 +1,31 @@
.{
.fingerprint = 0xbe4b4e2ca9014040,
.name = .eazy_zig_tree_sitter,
.version = "0.0.0",
.minimum_zig_version = "0.16.0",
.dependencies = .{
.tree_sitter = .{
.url = "git+https://github.com/tree-sitter/zig-tree-sitter.git#b562510b5563dcf3b7fcac4a5fe9d60834c84667",
.hash = "tree_sitter-0.26.0-8heIf-vDAQDh0HhAAUVwVZFmLrwSKMZl5gRaVFqtas0W",
},
.tree_sitter_c = .{
.url = "git+https://github.com/tree-sitter/tree-sitter-c.git#b780e47fc780ddc8da13afa35a3f4ed5c157823d",
.hash = "tree_sitter_c-0.24.2-y5boS-ptQADHoCoVfjGT_nFtFQ5LbomIkW0fxG3_cmdB",
},
.tree_sitter_markdown = .{
.url = "git+https://github.com/tree-sitter-grammars/tree-sitter-markdown#c3570720f7f7bbad22fe96603f106276618e0cf5",
.hash = "N-V-__8AAPRhUwBMQ9zJ3yDPS8lwmNWjH6kmYlD6Bku8s6G0",
},
.tree_sitter_lua = .{
.url = "git+https://github.com/tree-sitter-grammars/tree-sitter-lua#10fe0054734eec83049514ea2e718b2a56acd0c9",
.hash = "N-V-__8AAE5ZCQA-BW5BOioWVkGcPTjhC5x1Qv07BH3Xt3dR",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
"README.md",
},
}

85
src/main.zig Normal file
View file

@ -0,0 +1,85 @@
const std = @import("std");
const Io = std.Io;
const ts = @import("tree-sitter");
const ezts = @import("eazy_zig_tree_sitter");
const test_source =
\\#include <stdio.h>
\\
\\int add(int a, int b) {
\\ return a + b;
\\}
\\
\\int main(void) {
\\ int result = add(1, 2);
\\ printf("result: %d\n", result);
\\ return 0;
\\}
\\
;
const test_source_lua =
\\local function values()
\\ return 3, 7
\\end
\\
\\a, b = values()
\\print(a)
\\print(b)
\\
\\_, c = values()
\\print(c)
;
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const query_source = ezts.getQuery("lua").?;
const language = ezts.getLang("lua").?;
defer language.destroy();
const parser = ts.Parser.create();
defer parser.destroy();
try parser.setLanguage(language);
const tree = parser.parseString(test_source_lua, null);
defer tree.?.destroy();
const root = tree.?.rootNode();
var error_offset: u32 = 0;
const query = ts.Query.create(language, query_source, &error_offset) catch |err| {
std.debug.print("failed to parse query at byte {d}: {s}\n", .{ error_offset, @errorName(err) });
return err;
};
defer query.destroy();
const cursor = ts.QueryCursor.create();
defer cursor.destroy();
cursor.exec(query, root);
const Span = struct {
start: u32,
end: u32,
name: []const u8,
};
var spans: std.ArrayList(Span) = .empty;
defer spans.deinit(allocator);
while (cursor.nextCapture()) |result| {
const capture_index, const match = result;
const capture = match.captures[capture_index];
const name = query.captureNameForId(capture.index) orelse "?";
std.debug.print("{d} {d} {s}\n", .{ capture.node.startByte(), capture.node.endByte(), name });
try spans.append(allocator, .{
.start = capture.node.startByte(),
.end = capture.node.endByte(),
.name = name,
});
}
}
test { _ = @import("tests.zig"); }

15
src/root.zig Normal file
View file

@ -0,0 +1,15 @@
const std = @import("std");
const Io = std.Io;
const ts = @import("tree-sitter");
pub const grammars = @import("grammars");
pub const queries = @import("queries");
pub fn getLang(lang: []const u8) ?*ts.Language {
return (grammars.languages.get(lang) orelse return null)();
}
pub fn getQuery(lang: []const u8) ?[]const u8 {
return queries.queries.get(lang);
}