summary refs log tree commit diff
path: root/src/selection.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/selection.zig')
-rw-r--r--src/selection.zig186
1 files changed, 186 insertions, 0 deletions
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);
+    }
+};