Initial release

This commit is contained in:
CJ van den Berg 2024-02-20 17:59:34 +01:00
parent 84a25cc089
commit 395374409c
10 changed files with 1301 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.cache/
/zig-cache/
/zig-out/

99
build.zig Normal file
View file

@ -0,0 +1,99 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const tree_sitter_dep = b.dependency("tree-sitter", .{ .target = target, .optimize = optimize });
const clap_dep = b.dependency("clap", .{ .target = target, .optimize = optimize });
const themes_dep = b.dependency("themes", .{});
const syntax_mod = b.createModule(.{
.root_source_file = .{ .path = "src/syntax.zig" },
.imports = &.{
.{ .name = "tree-sitter", .module = tree_sitter_dep.module("tree-sitter") },
file_module(b, tree_sitter_dep, "tree-sitter-agda/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-bash/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-c-sharp/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-c/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-cpp/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-css/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-diff/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-dockerfile/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-git-rebase/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-go/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-fish/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-haskell/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-html/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-java/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-javascript/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-jsdoc/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-json/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-lua/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-make/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-nasm/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-ninja/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-nix/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-ocaml/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-openscad/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-php/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-python/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-purescript/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-regex/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-ruby/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-rust/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-ssh-config/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-scala/queries/scala/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-scheme/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-toml/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-typescript/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-zig/queries/highlights.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-cpp/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-html/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-javascript/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-lua/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-nasm/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-nix/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-openscad/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-php/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-purescript/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-purescript/vim_queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-rust/queries/injections.scm"),
file_module(b, tree_sitter_dep, "tree-sitter-zig/queries/injections.scm"),
},
});
const exe = b.addExecutable(.{
.name = "zat",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("syntax", syntax_mod);
exe.root_module.addImport("theme", themes_dep.module("theme"));
exe.root_module.addImport("themes", themes_dep.module("themes"));
exe.root_module.addImport("clap", clap_dep.module("clap"));
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
fn file_module(b: *std.Build, dep: *std.Build.Dependency, comptime sub_path: []const u8) std.Build.Module.Import {
return .{
.name = sub_path,
.module = b.createModule(.{
.root_source_file = dep.path(sub_path),
}),
};
}

23
build.zig.zon Normal file
View file

@ -0,0 +1,23 @@
.{
.name = "zat",
.version = "0.0.1",
.dependencies = .{
.@"tree-sitter" = .{
.url = "https://github.com/neurocyte/tree-sitter/releases/download/master-1ab391b43aa06e8648108f4d194e769a55a187be/source.tar.gz",
.hash = "12209d0d502f6ee33386c3369d87936b919d56f0302bc030e6deee5d806451a4ca34",
},
.clap = .{
.url = "https://github.com/Hejsil/zig-clap/archive/9c23bcb5aebe0c2542b4de4472f60959974e2222.tar.gz",
.hash = "12209e829da9d7d0bc089e4e0cbc07bb882f6192cd583277277da34df53cd05b8f2a",
},
.themes = .{
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-9ee6d7bc28256202aa7b3b20555bf480715c4e5c/flow-themes.tar.gz",
.hash = "1220cd21ee1f3e194f1cca5d52175c20d2c663a53eadaa9057997e72aa828f5d3864",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

2
src/ansi-term.zig Normal file
View file

@ -0,0 +1,2 @@
pub const style = @import("ansi-term/style.zig");
pub const format = @import("ansi-term/format.zig");

304
src/ansi-term/format.zig Normal file
View file

@ -0,0 +1,304 @@
const std = @import("std");
const fixedBufferStream = std.io.fixedBufferStream;
const testing = std.testing;
const style = @import("style.zig");
const Style = style.Style;
const FontStyle = style.FontStyle;
const Color = style.Color;
const esc = "\x1B";
const csi = esc ++ "[";
const reset = csi ++ "0m";
const font_style_codes = std.ComptimeStringMap([]const u8, .{
.{ "bold", "1" },
.{ "dim", "2" },
.{ "italic", "3" },
.{ "underline", "4" },
.{ "slowblink", "5" },
.{ "rapidblink", "6" },
.{ "reverse", "7" },
.{ "hidden", "8" },
.{ "crossedout", "9" },
.{ "fraktur", "20" },
.{ "overline", "53" },
});
/// Update the current style of the ANSI terminal
///
/// Optionally accepts the previous style active on the
/// terminal. Using this information, the function will update only
/// the attributes which are new in order to minimize the amount
/// written.
///
/// Tries to use as little bytes as necessary. Use this function if
/// you want to optimize for smallest amount of transmitted bytes
/// instead of computation speed.
pub fn updateStyle(writer: anytype, new: Style, old: ?Style) !void {
if (old) |sty| if (new.eql(sty)) return;
if (new.isDefault()) return try resetStyle(writer);
// A reset is required if the new font style has attributes not
// present in the old style or if the old style is not known
const reset_required = if (old) |sty| !sty.font_style.subsetOf(new.font_style) else true;
if (reset_required) try resetStyle(writer);
// Start the escape sequence
try writer.writeAll(csi);
var written_something = false;
// Font styles
const write_styles = if (reset_required) new.font_style else new.font_style.without(old.?.font_style);
inline for (std.meta.fields(FontStyle)) |field| {
if (@field(write_styles, field.name)) {
const code = font_style_codes.get(field.name).?;
if (written_something) {
try writer.writeAll(";");
} else {
written_something = true;
}
try writer.writeAll(code);
}
}
// Foreground color
if (reset_required and new.foreground != .Default or old != null and !old.?.foreground.eql(new.foreground)) {
if (written_something) {
try writer.writeAll(";");
} else {
written_something = true;
}
switch (new.foreground) {
.Default => try writer.writeAll("39"),
.Black => try writer.writeAll("30"),
.Red => try writer.writeAll("31"),
.Green => try writer.writeAll("32"),
.Yellow => try writer.writeAll("33"),
.Blue => try writer.writeAll("34"),
.Magenta => try writer.writeAll("35"),
.Cyan => try writer.writeAll("36"),
.White => try writer.writeAll("37"),
.Fixed => |fixed| try writer.print("38;5;{}", .{fixed}),
.Grey => |grey| try writer.print("38;2;{};{};{}", .{ grey, grey, grey }),
.RGB => |rgb| try writer.print("38;2;{};{};{}", .{ rgb.r, rgb.g, rgb.b }),
}
}
// Background color
if (reset_required and new.background != .Default or old != null and !old.?.background.eql(new.background)) {
if (written_something) {
try writer.writeAll(";");
} else {
written_something = true;
}
switch (new.background) {
.Default => try writer.writeAll("49"),
.Black => try writer.writeAll("40"),
.Red => try writer.writeAll("41"),
.Green => try writer.writeAll("42"),
.Yellow => try writer.writeAll("43"),
.Blue => try writer.writeAll("44"),
.Magenta => try writer.writeAll("45"),
.Cyan => try writer.writeAll("46"),
.White => try writer.writeAll("47"),
.Fixed => |fixed| try writer.print("48;5;{}", .{fixed}),
.Grey => |grey| try writer.print("48;2;{};{};{}", .{ grey, grey, grey }),
.RGB => |rgb| try writer.print("48;2;{};{};{}", .{ rgb.r, rgb.g, rgb.b }),
}
}
// End the escape sequence
try writer.writeAll("m");
}
test "same style default, no update" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{}, Style{});
const expected = "";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "same style non-default, no update" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
const sty = Style{
.foreground = Color.Green,
};
try updateStyle(fixed_buf_stream.writer(), sty, sty);
const expected = "";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "reset to default, old null" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{}, null);
const expected = "\x1B[0m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "reset to default, old non-null" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{}, Style{
.font_style = FontStyle.bold,
});
const expected = "\x1B[0m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "bold style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.font_style = FontStyle.bold,
}, Style{});
const expected = "\x1B[1m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "add bold style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.font_style = FontStyle{ .bold = true, .italic = true },
}, Style{
.font_style = FontStyle.italic,
});
const expected = "\x1B[1m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "reset required font style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.font_style = FontStyle.bold,
}, Style{
.font_style = FontStyle{ .bold = true, .underline = true },
});
const expected = "\x1B[0m\x1B[1m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "reset required color style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.foreground = Color.Red,
}, null);
const expected = "\x1B[0m\x1B[31m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "no reset required color style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.foreground = Color.Red,
}, Style{});
const expected = "\x1B[31m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "no reset required add color style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try updateStyle(fixed_buf_stream.writer(), Style{
.foreground = Color.Red,
.background = Color.Magenta,
}, Style{
.background = Color.Magenta,
});
const expected = "\x1B[31m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
pub fn resetStyle(writer: anytype) !void {
try writer.writeAll(reset);
}
test "reset style" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
try resetStyle(fixed_buf_stream.writer());
const expected = "\x1B[0m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "Grey foreground color" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
var new_style = Style{};
new_style.foreground = Color{ .Grey = 1 };
try updateStyle(fixed_buf_stream.writer(), new_style, Style{});
const expected = "\x1B[38;2;1;1;1m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}
test "Grey background color" {
var buf: [1024]u8 = undefined;
var fixed_buf_stream = fixedBufferStream(&buf);
var new_style = Style{};
new_style.background = Color{ .Grey = 1 };
try updateStyle(fixed_buf_stream.writer(), new_style, Style{});
const expected = "\x1B[48;2;1;1;1m";
const actual = fixed_buf_stream.getWritten();
try testing.expectEqualSlices(u8, expected, actual);
}

220
src/ansi-term/style.zig Normal file
View file

@ -0,0 +1,220 @@
const std = @import("std");
const meta = std.meta;
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
pub const ColorRGB = struct {
r: u8,
g: u8,
b: u8,
const Self = @This();
pub fn eql(self: Self, other: Self) bool {
return meta.eql(self, other);
}
};
pub const Color = union(enum) {
Default,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Fixed: u8,
Grey: u8,
RGB: ColorRGB,
const Self = @This();
pub fn eql(self: Self, other: Self) bool {
return meta.eql(self, other);
}
};
pub const FontStyle = packed struct {
bold: bool = false,
dim: bool = false,
italic: bool = false,
underline: bool = false,
slowblink: bool = false,
rapidblink: bool = false,
reverse: bool = false,
hidden: bool = false,
crossedout: bool = false,
fraktur: bool = false,
overline: bool = false,
const Self = @This();
pub const bold = Self{
.bold = true,
};
pub const dim = Self{
.dim = true,
};
pub const italic = Self{
.italic = true,
};
pub const underline = Self{
.underline = true,
};
pub const slowblink = Self{
.slowblink = true,
};
pub const rapidblink = Self{
.rapidblink = true,
};
pub const reverse = Self{
.reverse = true,
};
pub const hidden = Self{
.hidden = true,
};
pub const crossedout = Self{
.crossedout = true,
};
pub const fraktur = Self{
.fraktur = true,
};
pub const overline = Self{
.overline = true,
};
pub fn toU11(self: Self) u11 {
return @bitCast(self);
}
pub fn fromU11(bits: u11) Self {
return @bitCast(bits);
}
/// Returns true iff this font style contains no attributes
pub fn isDefault(self: Self) bool {
return self.toU11() == 0;
}
/// Returns true iff these font styles contain exactly the same
/// attributes
pub fn eql(self: Self, other: Self) bool {
return self.toU11() == other.toU11();
}
/// Returns true iff self is a subset of the attributes of
/// other, i.e. all attributes of self are at least present in
/// other as well
pub fn subsetOf(self: Self, other: Self) bool {
return self.toU11() & other.toU11() == self.toU11();
}
/// Returns this font style with all attributes removed that are
/// contained in other
pub fn without(self: Self, other: Self) Self {
return fromU11(self.toU11() & ~other.toU11());
}
};
test "FontStyle bits" {
try expectEqual(@as(u11, 0), (FontStyle{}).toU11());
try expectEqual(@as(u11, 1), (FontStyle.bold).toU11());
try expectEqual(@as(u11, 1 << 2), (FontStyle.italic).toU11());
try expectEqual(@as(u11, 1 << 2) | 1, (FontStyle{ .bold = true, .italic = true }).toU11());
try expectEqual(FontStyle{}, FontStyle.fromU11((FontStyle{}).toU11()));
try expectEqual(FontStyle.bold, FontStyle.fromU11((FontStyle.bold).toU11()));
}
test "FontStyle subsetOf" {
const default = FontStyle{};
const bold = FontStyle.bold;
const italic = FontStyle.italic;
const bold_and_italic = FontStyle{ .bold = true, .italic = true };
try expect(default.subsetOf(default));
try expect(default.subsetOf(bold));
try expect(bold.subsetOf(bold));
try expect(!bold.subsetOf(default));
try expect(!bold.subsetOf(italic));
try expect(default.subsetOf(bold_and_italic));
try expect(bold.subsetOf(bold_and_italic));
try expect(italic.subsetOf(bold_and_italic));
try expect(bold_and_italic.subsetOf(bold_and_italic));
try expect(!bold_and_italic.subsetOf(bold));
try expect(!bold_and_italic.subsetOf(italic));
try expect(!bold_and_italic.subsetOf(default));
}
test "FontStyle without" {
const default = FontStyle{};
const bold = FontStyle.bold;
const italic = FontStyle.italic;
const bold_and_italic = FontStyle{ .bold = true, .italic = true };
try expectEqual(default, default.without(default));
try expectEqual(bold, bold.without(default));
try expectEqual(default, bold.without(bold));
try expectEqual(bold, bold.without(italic));
try expectEqual(bold, bold_and_italic.without(italic));
try expectEqual(italic, bold_and_italic.without(bold));
try expectEqual(default, bold_and_italic.without(bold_and_italic));
}
pub const Style = struct {
foreground: Color = .Default,
background: Color = .Default,
font_style: FontStyle = FontStyle{},
const Self = @This();
/// Returns true iff this style equals the other style in
/// foreground color, background color and font style
pub fn eql(self: Self, other: Self) bool {
if (!self.font_style.eql(other.font_style))
return false;
if (!meta.eql(self.foreground, other.foreground))
return false;
return meta.eql(self.background, other.background);
}
/// Returns true iff this style equals the default set of styles
pub fn isDefault(self: Self) bool {
return eql(self, Self{});
}
// pub const parse = @import("parse_style.zig").parseStyle;
};
test "style equality" {
const a = Style{};
const b = Style{
.font_style = FontStyle.bold,
};
const c = Style{
.foreground = Color.Red,
};
try expect(a.isDefault());
try expect(a.eql(a));
try expect(b.eql(b));
try expect(c.eql(c));
try expect(!a.eql(b));
try expect(!b.eql(a));
try expect(!a.eql(c));
}

119
src/file_type.zig Normal file
View file

@ -0,0 +1,119 @@
const std = @import("std");
const ts = @import("tree-sitter");
pub const FileType = @This();
color: u24,
icon: []const u8,
name: []const u8,
lang_fn: LangFn,
extensions: []const []const u8,
highlights: [:0]const u8,
injections: ?[:0]const u8,
first_line_matches: ?FirstLineMatch = null,
comment: []const u8,
pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType {
if (guess_first_line(content)) |ft| return ft;
for (file_types) |*file_type|
if (file_path) |fp| if (match_file_type(file_type, fp))
return file_type;
return null;
}
fn guess_first_line(content: []const u8) ?*const FileType {
const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content;
for (file_types) |*file_type|
if (file_type.first_line_matches) |match|
if (match_first_line(match, first_line))
return file_type;
return null;
}
fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool {
if (match.prefix) |prefix|
if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix))
return false;
if (match.content) |content|
if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false;
return true;
}
fn match_file_type(file_type: *const FileType, file_path: []const u8) bool {
const basename = std.fs.path.basename(file_path);
const extension = std.fs.path.extension(file_path);
return for (file_type.extensions) |ext| {
if (ext.len == basename.len and std.mem.eql(u8, ext, basename))
return true;
if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..]))
return true;
} else false;
}
pub fn Parser(comptime lang: []const u8) LangFn {
return get_parser(lang);
}
fn get_parser(comptime lang: []const u8) LangFn {
const language_name = ft_func_name(lang);
return @extern(?LangFn, .{ .name = "tree_sitter_" ++ language_name }) orelse @compileError(std.fmt.comptimePrint("Cannot find extern tree_sitter_{s}", .{language_name}));
}
fn ft_func_name(comptime lang: []const u8) []const u8 {
var func_name: [lang.len]u8 = undefined;
for (lang, 0..) |c, i|
func_name[i] = if (c == '-') '_' else c;
return &func_name;
}
const LangFn = *const fn () callconv(.C) ?*const ts.Language;
const FirstLineMatch = struct {
prefix: ?[]const u8 = null,
content: ?[]const u8 = null,
};
const FileTypeOptions = struct {
extensions: []const []const u8 = &[_][]const u8{},
comment: []const u8,
icon: ?[]const u8 = null,
color: ?u24 = null,
highlights: ?[:0]const u8 = null,
injections: ?[:0]const u8 = null,
first_line_matches: ?FirstLineMatch = null,
parser: ?LangFn = null,
};
fn DeclLang(comptime lang: []const u8, comptime args: FileTypeOptions) FileType {
return .{
.color = args.color orelse 0xffffff,
.icon = args.icon orelse "󱀫",
.name = lang,
.lang_fn = if (args.parser) |p| p else get_parser(lang),
.extensions = args.extensions,
.comment = args.comment,
.highlights = if (args.highlights) |h| h else @embedFile("tree-sitter-" ++ lang ++ "/queries/highlights.scm"),
.injections = args.injections,
};
}
const file_types = load_file_types(@import("file_types.zig"));
fn load_file_types(comptime Namespace: type) []FileType {
comptime switch (@typeInfo(Namespace)) {
.Struct => |info| {
var count = 0;
for (info.decls) |_| {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
count += 1;
}
var cmds: [count]FileType = undefined;
var i = 0;
for (info.decls) |decl| {
cmds[i] = DeclLang(decl.name, @field(Namespace, decl.name));
i += 1;
}
return &cmds;
},
else => @compileError("expected tuple or struct type"),
};
}

262
src/file_types.zig Normal file
View file

@ -0,0 +1,262 @@
pub const agda = .{
.extensions = &[_][]const u8{"agda"},
.comment = "--",
};
pub const bash = .{
.color = 0x3e474a,
.icon = "󱆃",
.extensions = &[_][]const u8{ "sh", "bash" },
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "/bin/bash" },
};
pub const c = .{
.icon = "󰙱",
.extensions = &[_][]const u8{ "c", "h" },
.comment = "//",
};
pub const @"c-sharp" = .{
.color = 0x68217a,
.icon = "󰌛",
.extensions = &[_][]const u8{"cs"},
.comment = "//",
};
pub const conf = .{
.color = 0x000000,
.icon = "",
.extensions = &[_][]const u8{ "conf", "config" },
.highlights = fish.highlights,
.comment = "#",
.parser = fish.parser,
};
pub const cpp = .{
.color = 0x9c033a,
.icon = "",
.extensions = &[_][]const u8{ "cc", "cpp", "cxx", "hpp", "hxx", "h" },
.comment = "//",
.injections = @embedFile("tree-sitter-cpp/queries/injections.scm"),
};
pub const css = .{
.color = 0x3d8fc6,
.icon = "󰌜",
.extensions = &[_][]const u8{"css"},
.comment = "//",
};
pub const diff = .{
.extensions = &[_][]const u8{"diff"},
.comment = "#",
};
pub const dockerfile = .{
.color = 0x019bc6,
.icon = "",
.extensions = &[_][]const u8{ "Dockerfile", "dockerfile", "docker", "Containerfile", "container" },
.comment = "#",
};
pub const fish = .{
.extensions = &[_][]const u8{"fish"},
.comment = "#",
.parser = @import("file_type.zig").Parser("fish"),
.highlights = @embedFile("tree-sitter-fish/queries/highlights.scm"),
};
pub const @"git-rebase" = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"git-rebase-todo"},
.comment = "#",
};
pub const gitcommit = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"COMMIT_EDITMSG"},
.comment = "#",
.injections = @embedFile("tree-sitter-gitcommit/queries/injections.scm"),
};
pub const go = .{
.color = 0x00acd7,
.icon = "󰟓",
.extensions = &[_][]const u8{"go"},
.comment = "//",
};
pub const haskell = .{
.color = 0x5E5185,
.icon = "󰲒",
.extensions = &[_][]const u8{"hs"},
.comment = "--",
};
pub const html = .{
.color = 0xe54d26,
.icon = "󰌝",
.extensions = &[_][]const u8{"html"},
.comment = "<!--",
.injections = @embedFile("tree-sitter-html/queries/injections.scm"),
};
pub const java = .{
.color = 0xEA2D2E,
.icon = "",
.extensions = &[_][]const u8{"java"},
.comment = "//",
};
pub const javascript = .{
.color = 0xf0db4f,
.icon = "󰌞",
.extensions = &[_][]const u8{"js"},
.comment = "//",
.injections = @embedFile("tree-sitter-javascript/queries/injections.scm"),
};
pub const json = .{
.extensions = &[_][]const u8{"json"},
.comment = "//",
};
pub const lua = .{
.color = 0x000080,
.icon = "󰢱",
.extensions = &[_][]const u8{"lua"},
.comment = "--",
.injections = @embedFile("tree-sitter-lua/queries/injections.scm"),
};
pub const make = .{
.extensions = &[_][]const u8{ "makefile", "Makefile", "MAKEFILE", "GNUmakefile", "mk", "mak", "dsp" },
.comment = "#",
};
pub const markdown = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{"md"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"),
};
pub const @"markdown-inline" = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"),
};
pub const nasm = .{
.extensions = &[_][]const u8{ "asm", "nasm" },
.comment = "#",
.injections = @embedFile("tree-sitter-nasm/queries/injections.scm"),
};
pub const ninja = .{
.extensions = &[_][]const u8{"ninja"},
.comment = "#",
};
pub const nix = .{
.color = 0x5277C3,
.icon = "󱄅",
.extensions = &[_][]const u8{"nix"},
.comment = "#",
.injections = @embedFile("tree-sitter-nix/queries/injections.scm"),
};
pub const ocaml = .{
.color = 0xF18803,
.icon = "",
.extensions = &[_][]const u8{ "ml", "mli" },
.comment = "(*",
};
pub const openscad = .{
.color = 0x000000,
.icon = "󰻫",
.extensions = &[_][]const u8{"scad"},
.comment = "//",
.injections = @embedFile("tree-sitter-openscad/queries/injections.scm"),
};
pub const php = .{
.color = 0x6181b6,
.icon = "󰌟",
.extensions = &[_][]const u8{"php"},
.comment = "//",
.injections = @embedFile("tree-sitter-php/queries/injections.scm"),
};
// pub const purescript = .{
// .extensions = &[_][]const u8{"purs"},
// .comment = "--",
// .injections = @embedFile("tree-sitter-purescript/queries/injections.scm"),
// };
pub const python = .{
.color = 0xffd845,
.icon = "󰌠",
.extensions = &[_][]const u8{"py"},
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "/bin/bash" },
};
pub const regex = .{
.extensions = &[_][]const u8{},
.comment = "#",
};
pub const ruby = .{
.color = 0xd91404,
.icon = "󰴭",
.extensions = &[_][]const u8{"rb"},
.comment = "#",
};
pub const rust = .{
.color = 0x000000,
.icon = "󱘗",
.extensions = &[_][]const u8{"rs"},
.comment = "//",
.injections = @embedFile("tree-sitter-rust/queries/injections.scm"),
};
pub const scheme = .{
.extensions = &[_][]const u8{ "scm", "ss" },
.comment = ";",
};
pub const @"ssh-config" = .{
.extensions = &[_][]const u8{".ssh/config"},
.comment = "#",
};
pub const toml = .{
.extensions = &[_][]const u8{"toml"},
.comment = "#",
};
pub const typescript = .{
.color = 0x007acc,
.icon = "󰛦",
.extensions = &[_][]const u8{ "ts", "tsx" },
.comment = "//",
};
pub const zig = .{
.color = 0xf7a41d,
.icon = "",
.extensions = &[_][]const u8{ "zig", "zon" },
.comment = "//",
.injections = @embedFile("tree-sitter-zig/queries/injections.scm"),
};

198
src/main.zig Normal file
View file

@ -0,0 +1,198 @@
const std = @import("std");
const clap = @import("clap");
const syntax = @import("syntax");
const Theme = @import("theme");
const themes = @import("themes");
const term = @import("ansi-term.zig");
const StyleCache = std.AutoHashMap(u32, ?Theme.Token);
var style_cache: StyleCache = undefined;
pub fn main() !void {
const params = comptime clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\<str>... File to open.
\\
);
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
const a = gpa.allocator();
style_cache = StyleCache.init(a);
var diag = clap.Diagnostic{};
var res = clap.parse(clap.Help, &params, clap.parsers.default, .{
.diagnostic = &diag,
.allocator = a,
}) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}) catch {};
std.os.exit(1);
return err;
};
defer res.deinit();
if (res.args.help != 0)
return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
const theme = get_theme_by_name("default") orelse unreachable;
const stdout_file = std.io.getStdOut().writer();
var bw = std.io.bufferedWriter(stdout_file);
const writer = bw.writer();
if (res.positionals.len > 0) {
for (res.positionals) |arg| {
const file = try std.fs.cwd().openFile(arg, .{ .mode = .read_only });
defer file.close();
const content = try file.readToEndAlloc(a, std.math.maxInt(u32));
defer a.free(content);
try render_file(a, writer, content, arg, &theme);
try bw.flush();
}
} else {
const content = try std.io.getStdIn().readToEndAlloc(a, std.math.maxInt(u32));
defer a.free(content);
try render_file(a, writer, content, "-", &theme);
}
try bw.flush();
}
fn render_file(a: std.mem.Allocator, writer: anytype, content: []const u8, file_path: []const u8, theme: *const Theme) !void {
const parser = try syntax.create_guess_file_type(a, content, file_path);
const Ctx = struct {
writer: @TypeOf(writer),
content: []const u8,
theme: *const Theme,
last_pos: usize = 0,
fn cb(ctx: *@This(), range: syntax.Range, scope: []const u8, id: u32, idx: usize) error{Stop}!void {
defer ctx.last_pos = range.end_byte;
if (idx > 0) return;
if (ctx.last_pos < range.start_byte)
ctx.writer.writeAll(ctx.content[ctx.last_pos..range.start_byte]) catch return error.Stop;
if (range.start_byte < ctx.last_pos) return;
const style_ = style_cache_lookup(ctx.theme, scope, id);
const style = if (style_) |sty| sty.style else {
ctx.writer.writeAll(ctx.content[range.start_byte..range.end_byte]) catch return error.Stop;
return;
};
set_ansi_style(ctx.writer, style) catch return error.Stop;
ctx.writer.writeAll(ctx.content[range.start_byte..range.end_byte]) catch return error.Stop;
}
};
var ctx: Ctx = .{ .writer = writer, .content = content, .theme = theme };
try parser.render(&ctx, Ctx.cb);
}
fn style_cache_lookup(theme: *const Theme, scope: []const u8, id: u32) ?Theme.Token {
return if (style_cache.get(id)) |sty| ret: {
break :ret sty;
} else ret: {
const sty = find_scope_style(theme, scope) orelse null;
style_cache.put(id, sty) catch {};
break :ret sty;
};
}
fn find_scope_style(theme: *const Theme, scope: []const u8) ?Theme.Token {
return if (find_scope_fallback(scope)) |tm_scope|
find_scope_style_nofallback(theme, tm_scope) orelse find_scope_style_nofallback(theme, scope)
else
find_scope_style_nofallback(theme, scope);
}
fn find_scope_style_nofallback(theme: *const Theme, scope: []const u8) ?Theme.Token {
var idx = theme.tokens.len - 1;
var done = false;
while (!done) : (if (idx == 0) {
done = true;
} else {
idx -= 1;
}) {
const token = theme.tokens[idx];
const name = themes.scopes[token.id];
if (name.len > scope.len)
continue;
if (std.mem.eql(u8, name, scope[0..name.len]))
return token;
}
return null;
}
fn find_scope_fallback(scope: []const u8) ?[]const u8 {
for (fallbacks) |fallback| {
if (fallback.ts.len > scope.len)
continue;
if (std.mem.eql(u8, fallback.ts, scope[0..fallback.ts.len]))
return fallback.tm;
}
return null;
}
pub const FallBack = struct { ts: []const u8, tm: []const u8 };
pub const fallbacks: []const FallBack = &[_]FallBack{
.{ .ts = "namespace", .tm = "entity.name.namespace" },
.{ .ts = "type", .tm = "entity.name.type" },
.{ .ts = "type.defaultLibrary", .tm = "support.type" },
.{ .ts = "struct", .tm = "storage.type.struct" },
.{ .ts = "class", .tm = "entity.name.type.class" },
.{ .ts = "class.defaultLibrary", .tm = "support.class" },
.{ .ts = "interface", .tm = "entity.name.type.interface" },
.{ .ts = "enum", .tm = "entity.name.type.enum" },
.{ .ts = "function", .tm = "entity.name.function" },
.{ .ts = "function.defaultLibrary", .tm = "support.function" },
.{ .ts = "method", .tm = "entity.name.function.member" },
.{ .ts = "macro", .tm = "entity.name.function.macro" },
.{ .ts = "variable", .tm = "variable.other.readwrite , entity.name.variable" },
.{ .ts = "variable.readonly", .tm = "variable.other.constant" },
.{ .ts = "variable.readonly.defaultLibrary", .tm = "support.constant" },
.{ .ts = "parameter", .tm = "variable.parameter" },
.{ .ts = "property", .tm = "variable.other.property" },
.{ .ts = "property.readonly", .tm = "variable.other.constant.property" },
.{ .ts = "enumMember", .tm = "variable.other.enummember" },
.{ .ts = "event", .tm = "variable.other.event" },
// zig
.{ .ts = "attribute", .tm = "keyword" },
.{ .ts = "number", .tm = "constant.numeric" },
.{ .ts = "conditional", .tm = "keyword.control.conditional" },
.{ .ts = "operator", .tm = "keyword.operator" },
.{ .ts = "boolean", .tm = "keyword.constant.bool" },
.{ .ts = "string", .tm = "string.quoted" },
.{ .ts = "repeat", .tm = "keyword.control.flow" },
.{ .ts = "field", .tm = "variable" },
};
fn get_theme_by_name(name: []const u8) ?Theme {
for (themes.themes) |theme| {
if (std.mem.eql(u8, theme.name, name))
return theme;
}
return null;
}
fn set_ansi_style(writer: anytype, style: Theme.Style) !void {
const ansi_style = .{
.foreground = if (style.fg) |color| to_rgb_color(color) else .Default,
.background = if (style.bg) |color| to_rgb_color(color) else .Default,
.font_style = switch (style.fs orelse .normal) {
.normal => term.style.FontStyle{},
.bold => term.style.FontStyle.bold,
.italic => term.style.FontStyle.italic,
.underline => term.style.FontStyle.underline,
.strikethrough => term.style.FontStyle.crossedout,
},
};
try term.format.updateStyle(writer, ansi_style, null);
}
fn to_rgb_color(color: u24) term.style.Color {
const r = @as(u8, @intCast(color >> 16 & 0xFF));
const g = @as(u8, @intCast(color >> 8 & 0xFF));
const b = @as(u8, @intCast(color & 0xFF));
return .{ .RGB = .{ .r = r, .g = g, .b = b } };
}

71
src/syntax.zig Normal file
View file

@ -0,0 +1,71 @@
const std = @import("std");
const ts = @import("tree-sitter");
const Self = @This();
pub const Edit = ts.InputEdit;
pub const FileType = @import("file_type.zig");
pub const Range = ts.Range;
a: std.mem.Allocator,
lang: *const ts.Language,
file_type: *const FileType,
parser: *ts.Parser,
query: *ts.Query,
injections: *ts.Query,
tree: ?*ts.Tree = null,
content: []const u8,
pub fn create(file_type: *const FileType, a: std.mem.Allocator, content: []const u8) !*Self {
const self = try a.create(Self);
self.* = .{
.a = a,
.lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {d}", .{file_type.name}),
.file_type = file_type,
.parser = try ts.Parser.create(),
.query = try ts.Query.create(self.lang, file_type.highlights),
.injections = try ts.Query.create(self.lang, file_type.highlights),
.content = content,
};
errdefer self.destroy();
try self.parser.setLanguage(self.lang);
try self.parse();
return self;
}
pub fn create_guess_file_type(a: std.mem.Allocator, content: []const u8, file_path: ?[]const u8) !*Self {
const file_type = FileType.guess(file_path, content) orelse return error.NotFound;
return create(file_type, a, content);
}
pub fn destroy(self: *Self) void {
if (self.tree) |tree| tree.destroy();
self.query.destroy();
self.parser.destroy();
self.a.destroy(self);
}
fn parse(self: *Self) !void {
if (self.tree) |tree| tree.destroy();
self.tree = try self.parser.parseString(null, self.content);
}
fn CallBack(comptime T: type) type {
return fn (ctx: T, sel: Range, scope: []const u8, id: u32, idx: usize) error{Stop}!void;
}
pub fn render(self: *const Self, ctx: anytype, comptime cb: CallBack(@TypeOf(ctx))) !void {
const cursor = try ts.Query.Cursor.create();
defer cursor.destroy();
const tree = if (self.tree) |p| p else return;
cursor.execute(self.query, tree.getRootNode());
while (cursor.nextMatch()) |match| {
var idx: usize = 0;
for (match.captures()) |capture| {
const range = capture.node.getRange();
const scope = self.query.getCaptureNameForId(capture.id);
try cb(ctx, range, scope, capture.id, idx);
idx += 1;
}
}
}