From 395374409c02bf4cf31fe1b63ad292d7cd8efb35 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 20 Feb 2024 17:59:34 +0100 Subject: [PATCH] Initial release --- .gitignore | 3 + build.zig | 99 +++++++++++++ build.zig.zon | 23 +++ src/ansi-term.zig | 2 + src/ansi-term/format.zig | 304 +++++++++++++++++++++++++++++++++++++++ src/ansi-term/style.zig | 220 ++++++++++++++++++++++++++++ src/file_type.zig | 119 +++++++++++++++ src/file_types.zig | 262 +++++++++++++++++++++++++++++++++ src/main.zig | 198 +++++++++++++++++++++++++ src/syntax.zig | 71 +++++++++ 10 files changed, 1301 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/ansi-term.zig create mode 100644 src/ansi-term/format.zig create mode 100644 src/ansi-term/style.zig create mode 100644 src/file_type.zig create mode 100644 src/file_types.zig create mode 100644 src/main.zig create mode 100644 src/syntax.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29193e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.cache/ +/zig-cache/ +/zig-out/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..5f2ab28 --- /dev/null +++ b/build.zig @@ -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), + }), + }; +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..39f46b6 --- /dev/null +++ b/build.zig.zon @@ -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", + }, +} diff --git a/src/ansi-term.zig b/src/ansi-term.zig new file mode 100644 index 0000000..c9daf05 --- /dev/null +++ b/src/ansi-term.zig @@ -0,0 +1,2 @@ +pub const style = @import("ansi-term/style.zig"); +pub const format = @import("ansi-term/format.zig"); diff --git a/src/ansi-term/format.zig b/src/ansi-term/format.zig new file mode 100644 index 0000000..e11e032 --- /dev/null +++ b/src/ansi-term/format.zig @@ -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); +} diff --git a/src/ansi-term/style.zig b/src/ansi-term/style.zig new file mode 100644 index 0000000..a759273 --- /dev/null +++ b/src/ansi-term/style.zig @@ -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)); +} diff --git a/src/file_type.zig b/src/file_type.zig new file mode 100644 index 0000000..c35aacd --- /dev/null +++ b/src/file_type.zig @@ -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"), + }; +} diff --git a/src/file_types.zig b/src/file_types.zig new file mode 100644 index 0000000..d94c792 --- /dev/null +++ b/src/file_types.zig @@ -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 = "