summaryrefslogtreecommitdiff
path: root/src/normal.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/normal.zig')
-rw-r--r--src/normal.zig86
1 files changed, 78 insertions, 8 deletions
diff --git a/src/normal.zig b/src/normal.zig
index 2aea2e54..b68f9ba6 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -168,13 +168,9 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, mode: m
t.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO)
}
-/// helix's tree-sitter selection keys: every range walks the file's parse,
-/// which is made on the first of these keys after an edit and kept, never
-/// made again for a key that changed nothing. A range the tree has nothing
-/// to say about stays as it was; a text without a grammar, all of them.
-pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk) void {
- if (t.what != .body) return;
- const f = if (pane.file) |*file| file else return;
+/// The file's parse, made on the first key that walks it after an edit and
+/// kept until the next: never made again for a key that changed nothing.
+pub fn fileTree(p: *Pardes, f: *panes.File.State) ?*anyopaque {
if (f.node_tree == null or f.node_tree_revision != f.revision) {
if (f.node_tree) |old| syntax.freeTree(old);
f.node_tree = syntax.parseTree(f.path, f.content);
@@ -182,7 +178,40 @@ pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk
for (f.node_history.items) |step| p.gpa.free(step.ranges);
f.node_history.clearRetainingCapacity();
}
- const tree = f.node_tree orelse return;
+ return f.node_tree;
+}
+
+/// `]f`/`[f` and kin (helix goto_ts_object): to the next object starting
+/// after the cursor or the previous one ending before it, count times; select
+/// mode stretches the range to it.
+fn objectJump(p: *Pardes, pane: *Pane, t: *Text, text: []const u8, range: modal.Selection, object: modal.Normal.Object, forward: bool, count: u32) void {
+ if (t.what != .body) return;
+ const f = if (pane.file) |*file| file else return;
+ const tree = fileTree(p, f) orelse return;
+ var last = range;
+ for (0..count) |_| {
+ const s = syntax.objectNext(f.path, tree, text, object, forward, modal.selectionCursor(text, last)) orelse break;
+ const next: modal.Selection = .{ .anchor = s.from, .head = s.to };
+ if (std.meta.eql(next, last)) break;
+ last = next;
+ }
+ if (std.meta.eql(last, range)) return;
+ if (t.select) {
+ const head = if (last.head < range.anchor) last.anchor else last.head;
+ return t.setRange(text, 0, .{ .anchor = range.anchor, .head = head }, false);
+ }
+ const out: modal.Selection = if (forward) last else .{ .anchor = last.head, .head = last.anchor };
+ t.setRange(text, 0, out, false);
+}
+
+/// helix's tree-sitter selection keys: every range walks the file's parse,
+/// which is made on the first of these keys after an edit and kept, never
+/// made again for a key that changed nothing. A range the tree has nothing
+/// to say about stays as it was; a text without a grammar, all of them.
+pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk) void {
+ if (t.what != .body) return;
+ const f = if (pane.file) |*file| file else return;
+ const tree = fileTree(p, f) orelse return;
const text = f.content;
const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active;
const arena = p.scratch.allocator();
@@ -742,6 +771,7 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
),
.node => |walk| return nodeSelect(p, pane, t, walk),
.goto_word => return startJump(p, pane, t),
+ .object_jump => |jump| return objectJump(p, pane, t, text, range, jump.object, jump.direction == .forward, jump.count),
.macro_record => {
const m = &p.macro;
if (m.recording == 0) {
@@ -1041,3 +1071,43 @@ test "one cursor holds no room for other ranges; a second range makes it and one
try std.testing.expectEqual(@as(u16, 0), pane.body.nsel);
try std.testing.expectEqual(@as(usize, 0), pane.body.sels.len);
}
+
+test "tree-sitter textobject keys do in Python and JSON what hx 25.07.1 does" {
+ if (!syntax.supportsPath("a.py") or !syntax.supportsPath("a.json")) return;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ // each checked against the installed hx through a pty: the keys, then
+ // the text it saved
+ const Case = struct { path: []const u8, text: []const u8, keys: []const u8, want: []const u8 };
+ for ([_]Case{
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]fd", .want = "def alpha(a, b):\n return a + b\n\n\n\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]f]fd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "2]fd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "Gk[fd", .want = "ef alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "jmifd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "jmafd", .want = "\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "10lmiad", .want = "def alpha(, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "10lmaad", .want = "def alpha( b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]Td", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]cd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n \n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "v]fd", .want = "\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]td", .want = "ef alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]ad", .want = "def alpha( b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" },
+ .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "]ed", .want = "{, \"b\": [1, 2]}\n" },
+ .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "]e]ed", .want = "{\"a\": 1, }\n" },
+ .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "8lmaed", .want = "{\"a\": 1,\"b\": [1, 2]}\n" },
+ }) |c| {
+ const pane = try p.setTestFile(c.text);
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = try p.gpa.dupe(u8, c.path);
+ for (c.keys, 0..) |ch, i| {
+ p.update(.{ .key = .{ .cp = ch, .text = c.keys[i..][0..1] } });
+ while (p.nextEffect()) |_| {}
+ }
+ std.testing.expectEqualStrings(c.want, pane.file.?.content) catch |err| {
+ std.debug.print("keys {s} on {s}\n", .{ c.keys, c.path });
+ return err;
+ };
+ }
+}