summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--build.zig.zon8
-rw-r--r--src/main.janet25
-rw-r--r--src/main.zig184
-rw-r--r--src/selection.zig186
4 files changed, 349 insertions, 54 deletions
diff --git a/build.zig.zon b/build.zig.zon
index 7923a60..f62d31f 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -13,12 +13,12 @@
             .hash = "tree_sitter-0.25.0-8heIf51vAQConvVIgvm-9mVIbqh7yabZYqPXfOpS3YoG",
         },
         .vaxis = .{
-            .url = "git+https://github.com/rockorager/libvaxis#cc9154d5f4afa3fdfd289157f195ec67804ef437",
-            .hash = "vaxis-0.5.1-BWNV_MYNCQDO92x2PTpsfv6GwDHmjL98rf6iL3pbMLj4",
+            .url = "git+https://github.com/rockorager/libvaxis#025cf928ffc3ee7a4de36a91f4cca54a5810b2cc",
+            .hash = "vaxis-0.5.1-BWNV_CAVCQAmzxx2i2hBs5SvEupHwDbKHtE5VSOZwwtl",
         },
         .zsanett = .{
-            .url = "git+https://trong.loang.net/~cnx/zsanett#b2addf1901c1540797d740f35594df1ed7e72027",
-            .hash = "zsanett-0.0.0-1TMqaEbLAABbm3ueu1BMakaJ22PDqCGWpCA4yRYPacgB",
+            .url = "git+https://trong.loang.net/~cnx/zsanett#f5b2d6f3122b54fcdcb3bd6953420480bb9bc752",
+            .hash = "zsanett-0.0.0-1TMqaI7-AAD5R_t1Udvn92vPkwJnBQXnVzLgKMlJkOIB",
         },
     },
     .paths = .{
diff --git a/src/main.janet b/src/main.janet
new file mode 100644
index 0000000..8ca1215
--- /dev/null
+++ b/src/main.janet
@@ -0,0 +1,25 @@
+# Event loop
+# SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+(defn handle
+  [event]
+  (match event
+    [:key-press key] (cond
+                       (:matches key (chr "c") {:ctrl true}) [:quit]
+                       (:matches key (chr "h") {}) (do (:go-up env) [])
+                       (:matches key (chr "j") {}) (do (:go-right env) [])
+                       (:matches key (chr "k") {}) (do (:go-left env) [])
+                       (:matches key (chr "l") {}) (do (:go-down env) [])
+                       [])))
+
+(defn run
+  [events]
+  (if-let [event (array/pop events)]
+    (unless (= :quit event)
+      (let [next-events (handle event)]
+        (:render env)
+        (run (array/join events next-events))))
+    (run @[(:next-event env)])))
+
+(run @[])
diff --git a/src/main.zig b/src/main.zig
index aec275c..c2d9d12 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -4,23 +4,40 @@
 
 const Allocator = std.mem.Allocator;
 const DebugAllocator = std.heap.DebugAllocator;
+const argsAlloc = std.process.argsAlloc;
+const argsFree = std.process.argsFree;
 const builtin = @import("builtin");
+const cwd = std.fs.cwd;
 const eql = std.mem.eql;
+const maxInt = std.math.maxInt;
 const ns_per_s = std.time.ns_per_s;
 const smp_allocator = std.heap.smp_allocator;
 const std = @import("std");
 
 const Key = vaxis.Key;
 const Loop = vaxis.Loop;
+const Style = vaxis.Cell.Style;
 const Tty = vaxis.Tty;
 const Unicode = vaxis.Unicode;
+const Window = vaxis.Window;
 const Winsize = vaxis.Winsize;
 const gwidth = vaxis.gwidth.gwidth;
+const Vaxis = vaxis.Vaxis;
 const vaxis = @import("vaxis");
+
+const Parser = tree_sitter.Parser;
+const tree_sitter = @import("tree-sitter");
+
 const zsanett = @import("zsanett");
+pub fn zsanettIntern(T: type) bool {
+    return switch (T) {
+        Environment, Key, Loop(Event) => true,
+        else => false,
+    };
+}
 
 const Config = @import("Config.zig");
-const Token = @import("Token.zig");
+const selection = @import("selection.zig");
 const languages = @import("languages");
 
 const Event = union(enum) {
@@ -28,6 +45,88 @@ const Event = union(enum) {
     winsize: Winsize,
 };
 
+const Environment = struct {
+    allocator: Allocator,
+    loop: *Loop(Event),
+    parser: *Parser,
+    span: *selection.Span,
+    text: []const u8,
+    tty: *Tty,
+    vaxis: *Vaxis,
+
+    pub fn nextEvent(self: Environment) !Event {
+        while (true) {
+            const event = self.loop.nextEvent();
+            switch (event) {
+                .key_press => return event,
+                .winsize => |ws| {
+                    try self.vaxis.resize(self.allocator,
+                                          self.tty.anyWriter(), ws);
+                    try self.render();
+                },
+            }
+        }
+    }
+
+    pub fn render(self: Environment) !void {
+        const window = self.vaxis.window();
+        window.clear();
+        var col: u16 = 0;
+        var row: u16 = 0;
+        var graphemes = self.vaxis.unicode.graphemeIterator(self.text);
+        while (graphemes.next()) |grapheme| {
+            const style = Style {
+                .reverse = self.span.contains(grapheme),
+            };
+            const bytes = grapheme.bytes(self.text);
+            if (eql(u8, bytes, "\n")) {
+                const width = window.gwidth("$");
+                defer col = 0;
+                defer row += 1;
+                window.writeCell(col, row, .{
+                    .char = .{ .grapheme = "$", .width = @intCast(width) },
+                    .style = style,
+                });
+            } else if (eql(u8, bytes, "\t")) {
+                var tab = self.vaxis.unicode.graphemeIterator("  ");
+                while (tab.next()) |g| {
+                    const b = g.bytes("  ");
+                    const width = window.gwidth(b);
+                    defer col += width;
+                    window.writeCell(col, row, .{
+                        .char = .{ .grapheme = b, .width = @intCast(width) },
+                        .style = style,
+                    });
+                }
+            } else {
+                const width = window.gwidth(bytes);
+                defer col += width;
+                window.writeCell(col, row, .{
+                    .char = .{ .grapheme = bytes, .width = @intCast(width) },
+                    .style = style,
+                });
+            }
+        }
+        try self.vaxis.render(self.tty.anyWriter());
+    }
+
+    pub fn goUp(self: Environment) void {
+        self.span.goUp();
+    }
+
+    pub fn goRight(self: Environment) void {
+        self.span.goRight(self.vaxis.unicode.width_data.graphemes, self.text);
+    }
+
+    pub fn goLeft(self: Environment) void {
+        self.span.goLeft(self.vaxis.unicode.width_data.graphemes, self.text);
+    }
+
+    pub fn goDown(self: Environment) void {
+        self.span.goDown(self.vaxis.unicode.width_data.graphemes, self.text);
+    }
+};
+
 pub fn main() !void {
     var debug_allocator = DebugAllocator(.{}).init;
     defer _ = debug_allocator.deinit();
@@ -36,64 +135,49 @@ pub fn main() !void {
         .ReleaseFast, .ReleaseSmall => smp_allocator,
     };
 
-    zsanett.init();
-    defer zsanett.deinit();
-    const janet_env = zsanett.Environment.init(null);
-    const config = try Config.parse(allocator, janet_env);
-
     var tty = try Tty.init();
     defer tty.deinit();
     var vx = try vaxis.init(allocator, .{});
     defer vx.deinit(allocator, tty.anyWriter());
+
     var loop = Loop(Event){ .tty = &tty, .vaxis = &vx };
     try loop.init();
     try loop.start();
     defer loop.stop();
 
+    const parser = Parser.create();
+    defer parser.destroy();
+
+    const args = try argsAlloc(allocator);
+    defer argsFree(allocator, args);
+    const text = try cwd().readFileAlloc(allocator, args[1], maxInt(u32));
+    defer allocator.free(text);
     try vx.enterAltScreen(tty.anyWriter());
-    // For the alternate screen
-    try vx.queryTerminal(tty.anyWriter(), 1 * ns_per_s);
-
-    const text = "int main()\n{\n\treturn 0;\n}\n";
-    var tokens = try Token.ize(text, languages.c);
-    defer tokens.deinit();
-
-    while (true) {
-        switch (loop.nextEvent()) {
-            .key_press => |key| {
-                if (key.matches('c', .{ .ctrl = true })) {
-                    break;
-                }
-            },
-            .winsize => |ws| try vx.resize(allocator, tty.anyWriter(), ws),
-        }
-        const window = vx.window();
-        window.clear();
-        var col: u16 = 0;
-        var row: u16 = 0;
-        tokens.reset();
-        while (tokens.next()) |token| {
-            var graphemes = vx.unicode.graphemeIterator(token.text);
-            while (graphemes.next()) |grapheme| {
-                const bytes = grapheme.bytes(token.text);
-                if (eql(u8, bytes, "\r\n")
-                    or eql(u8, bytes, "\r")
-                    or eql(u8, bytes, "\n")) {
-                    col = 0;
-                    row += 1;
-                    continue;
-                }
-                const width: u8 = if (eql(u8, bytes, "\t"))
-                    config.tab_width
-                else
-                    @intCast(gwidth(bytes, vx.caps.unicode,
-                                    &vx.unicode.width_data));
-                defer col += width;
-                window.writeCell(col, row, .{
-                    .char = .{ .grapheme = bytes, .width = width },
-                });
-            }
-        }
-        try vx.render(tty.anyWriter());
+    try vx.queryTerminal(tty.anyWriter(), 1 * ns_per_s); // for alt screen
+
+    const language = languages.c();
+    defer language.destroy();
+    try parser.setLanguage(language);
+    const tree = parser.parseString(text, null).?;
+    defer tree.destroy();
+
+    zsanett.init();
+    defer zsanett.deinit();
+    var span = selection.Span{
+        .head = .{ .node = tree.rootNode() },
+        .tail = .{ .node = tree.rootNode() },
+    };
+    try zsanett.def("env", Environment{
+        .allocator = allocator,
+        .loop = &loop,
+        .parser = parser,
+        .span = &span,
+        .text = text,
+        .tty = &tty,
+        .vaxis = &vx,
+    }, "eval environment");
+    switch (try zsanett.eval(void, @embedFile("main.janet"), "main.janet")) {
+        .ret => {},
+        .err => |err| @panic(err),
     }
 }
diff --git a/src/selection.zig b/src/selection.zig
new file mode 100644
index 0000000..1fb8266
--- /dev/null
+++ b/src/selection.zig
@@ -0,0 +1,186 @@
+// Selection
+// SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+const eql = std.meta.eql;
+const order = std.math.order;
+const std = @import("std");
+
+const Grapheme = Graphemes.Grapheme;
+const Graphemes = vaxis.Graphemes;
+const Node = tree_sitter.Node;
+const Tree = tree_sitter.Tree;
+const tree_sitter = @import("tree-sitter");
+const vaxis = @import("vaxis");
+
+fn graphemeAt(graphemes: Graphemes, text: []const u8, index: u32) Grapheme {
+    var iterator = graphemes.iterator(text[index..]);
+    return .{ .offset = index, .len = iterator.peek().?.len };
+}
+
+fn graphemeBefore(graphemes: Graphemes, text: []const u8, index: u32) Grapheme {
+    var iterator = graphemes.reverseIterator(text[0..index]);
+    return iterator.peek().?;
+}
+
+// TODO: undo
+pub const Unit = union(enum) {
+    node: Node,
+    grapheme: struct {
+        const GraphemeUnit = @This();
+        slice: Grapheme,
+        tree: *const Tree,
+
+        fn parent(unit: GraphemeUnit) Node {
+            const start = unit.slice.offset;
+            const end = start + unit.slice.len;
+            return unit.tree.rootNode().descendantForByteRange(start, end).?;
+        }
+
+        fn at(unit: GraphemeUnit, graphemes: Graphemes,
+              text: []const u8, index: u32) GraphemeUnit {
+            return .{
+                .slice = graphemeAt(graphemes, text, index),
+                .tree = unit.tree,
+            };
+        }
+
+        fn before(unit: GraphemeUnit, graphemes: Graphemes,
+                  text: []const u8, index: u32) GraphemeUnit {
+            return .{
+                .slice = graphemeBefore(graphemes, text, index),
+                .tree = unit.tree,
+            };
+        }
+    },
+
+    pub fn up(unit: Unit) Unit {
+        return switch (unit) {
+            .node => |node| if (node.parent()) |parent_node|
+                if (parent_node.eql(node))
+                    .up(.{ .node = parent_node })
+                else
+                    .{ .node = parent_node }
+            else unit,
+            .grapheme => |grapheme| .{ .node = grapheme.parent() },
+        };
+    }
+
+    pub fn right(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+        switch (unit) {
+            .node => |node| if (node.nextSibling()) |sibling| {
+                const prev = node.endByte();
+                const next = sibling.startByte();
+                return switch (order(prev, next)) {
+                    .lt => .{
+                        .grapheme = .{
+                            .slice = graphemeAt(graphemes, text, prev),
+                            .tree = node.tree,
+                        }
+                    },
+                    .eq => .{ .node = sibling },
+                    .gt => unreachable,
+                };
+            } else return .up(unit),
+            .grapheme => |grapheme| {
+                const start = grapheme.slice.offset + grapheme.slice.len;
+                const next_grapheme = grapheme.at(graphemes, text, start);
+                const parent = next_grapheme.parent();
+                return if (parent.eql(grapheme.parent())) .{
+                    .grapheme = next_grapheme
+                } else .{
+                    .node = parent
+                };
+            },
+        }
+    }
+
+    pub fn left(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+        switch (unit) {
+            .node => |node| if (node.prevSibling()) |sibling| {
+                const prev = sibling.endByte();
+                const next = node.startByte();
+                return switch (order(prev, next)) {
+                    .lt => .{
+                        .grapheme = .{
+                            .slice = graphemeBefore(graphemes, text, next),
+                            .tree = node.tree,
+                        }
+                    },
+                    .eq => .{ .node = sibling },
+                    .gt => unreachable,
+                };
+            } else return .up(unit),
+            .grapheme => |grapheme| {
+                const end = grapheme.slice.offset;
+                const prev_grapheme = grapheme.before(graphemes, text, end);
+                const parent = prev_grapheme.parent();
+                return if (parent.eql(grapheme.parent())) .{
+                    .grapheme = prev_grapheme
+                } else .{
+                    .node = parent
+                };
+            },
+        }
+    }
+
+    pub fn down(unit: Unit, graphemes: Graphemes, text: []const u8) Unit {
+        switch (unit) {
+            .node => |node| {
+                const start = node.startByte();
+                const end = node.endByte();
+                // TODO: history
+                return if (node.child(0)) |child|
+                    if (child.startByte() == start and child.endByte() == end)
+                        .down(.{ .node = child }, graphemes, text)
+                    else
+                        .{ .node = child }
+                else .{
+                    .grapheme = .{
+                        .slice = graphemeAt(graphemes, text, start),
+                        .tree = node.tree,
+                    }
+                };
+            },
+            .grapheme => return unit,
+        }
+    }
+
+    pub fn contains(unit: Unit, other: Grapheme) bool {
+        return switch (unit) {
+            .node => |node| (node.startByte() <= other.offset
+                             and other.offset + other.len <= node.endByte()),
+            .grapheme => |grapheme| eql(grapheme.slice, other),
+        };
+    }
+};
+
+pub const Span = struct {
+    head: Unit,
+    tail: Unit,
+
+    pub fn contains(span: Span, grapheme: Grapheme) bool {
+        // TODO: range
+        return span.head.contains(grapheme);
+    }
+
+    pub fn goUp(span: *Span) void {
+        span.head = span.head.up();
+        span.tail = span.tail.up();
+    }
+
+    pub fn goRight(span: *Span, graphemes: Graphemes, text: []const u8) void {
+        span.head = span.head.right(graphemes, text);
+        span.tail = span.tail.right(graphemes, text);
+    }
+
+    pub fn goLeft(span: *Span, graphemes: Graphemes, text: []const u8) void {
+        span.head = span.head.left(graphemes, text);
+        span.tail = span.tail.left(graphemes, text);
+    }
+
+    pub fn goDown(span: *Span, graphemes: Graphemes, text: []const u8) void {
+        span.head = span.head.down(graphemes, text);
+        span.tail = span.tail.down(graphemes, text);
+    }
+};