commit 6a3f7b3fdee37b59388419f8aceb80ff72e81a21 Author: Grace Yoder Date: Wed Jul 15 15:19:45 2026 -0600 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8493d22 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c5954d --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8f724f --- /dev/null +++ b/README.md @@ -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) + + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..aa59884 --- /dev/null +++ b/build.zig @@ -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.?; + } +}; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..4f6f487 --- /dev/null +++ b/build.zig.zon @@ -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", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..55060d4 --- /dev/null +++ b/src/main.zig @@ -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 + \\ + \\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"); } diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..339f8f2 --- /dev/null +++ b/src/root.zig @@ -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); +}