summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 17:50:26 -0300
committerGabriel Schneider <[email protected]>2026-10-01 18:08:12 -0300
commit9dba9742f4f2071fd85c67c3a7fe311b7f37617c (patch)
tree0aaf9aa94dc6f185568db4b9b4ad500b9ae66943
parent8c74f9a8f199ea0c2d20cc524358d224ea192005 (diff)
downloadpardes-9dba9742f4f2071fd85c67c3a7fe311b7f37617c.tar.gz
pardes-9dba9742f4f2071fd85c67c3a7fe311b7f37617c.zip
Enter and Tab on a tag word tell an event reader as a click on it does, at its place in the tag; only a look takes a diff line's address, an exec there takes the word (follows vpmzwuzk)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/exec.zig12
-rw-r--r--src/look.zig62
-rw-r--r--src/mouse.zig18
-rw-r--r--src/pardes.zig16
4 files changed, 96 insertions, 12 deletions
diff --git a/src/exec.zig b/src/exec.zig
index 30a1a894..9d1b81d5 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -108,14 +108,14 @@ pub const PointerOperand = struct {
};
pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand {
- return operandAt(p, pane, clicked, true);
+ return operandAt(p, pane, clicked, true, true);
}
/// The body's explicit selection, else what a look at its cursor takes.
pub fn currentOperand(p: *Pardes, pane: *Pane) ?[]const u8 {
if (pane.file == null and !pane.isTerminal()) return null;
if ((pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active) return edit.currentSelText(p, pane);
- return operandAt(p, pane, look.cursorWordSel(p, pane), false).text;
+ return operandAt(p, pane, look.cursorWordSel(p, pane), false, true).text;
}
/// What a look or an exec at `clicked` takes: the ONE expansion every
@@ -123,8 +123,10 @@ pub fn currentOperand(p: *Pardes, pane: *Pane) ?[]const u8 {
/// Look), so a diff line, a hunk line's code word and a file word expand
/// alike however they are reached (look.zig's "every entry point" test).
/// `borrow`: a gesture inside a kept or modal selection takes it; the
-/// keyboard has said which selection it means already.
-pub fn operandAt(p: *Pardes, pane: *Pane, clicked: Pane.Sel, borrow: bool) PointerOperand {
+/// keyboard has said which selection it means already. `for_look`: only a
+/// look takes a diff line's address; an exec there takes the word, as
+/// anywhere else.
+pub fn operandAt(p: *Pardes, pane: *Pane, clicked: Pane.Sel, borrow: bool, for_look: bool) PointerOperand {
const visible = clicked.r0 - @as(i32, pane.tag_rows);
const wrapped = pane.wrapAt(visible);
const raw = pane.isTerminal() and pane.body.mode == .tty and clicked.r0 >= pane.tag_rows;
@@ -193,7 +195,7 @@ pub fn operandAt(p: *Pardes, pane: *Pane, clicked: Pane.Sel, borrow: bool) Point
// look from there. A hunk line's code is words, looked at as ever:
// the code's, the prefix never one of them.
var word_from: usize = 0;
- if (clicked.r0 >= pane.tag_rows and clicked.r0 == clicked.r1 and clicked.c0 == clicked.c1 and row >= 0) diff: {
+ if (for_look and clicked.r0 >= pane.tag_rows and clicked.r0 == clicked.r1 and clicked.c0 == clicked.c1 and row >= 0) diff: {
var address_buf: [4096 + 32]u8 = undefined;
const at: usize = @intCast(row);
const click = look.diffClick(p, pane, at, @intCast(@max(0, col)), &address_buf) orelse break :diff;
diff --git a/src/look.zig b/src/look.zig
index 0df1f307..a2aa5be0 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -2524,6 +2524,68 @@ test "every entry point to Look takes one expansion: a diff line and a file:12:5
}
}
+test "a tag word looked at by a click and by Enter is one record to an event reader; an exec on a diff line takes its word, never the address" {
+ if (!platform_has_fs) return;
+ const diff_text =
+ \\diff --git a/src/x.zig b/src/x.zig
+ \\--- a/src/x.zig
+ \\+++ b/src/x.zig
+ \\@@ -1,1 +1,1 @@
+ \\-old
+ \\+line one
+ \\
+ ;
+ var fx: DiffFixture = undefined;
+ try fx.init(true, &.{.{ "src/x.zig", "line one\n" }}, diff_text);
+ defer fx.tmp.cleanup();
+ const th = @import("ninep/testing.zig");
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const pane = try fx.open(p, diff_text);
+ pane.tag_expanded = true;
+ var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer frame.deinit();
+ p.sync();
+ _ = try p.render(frame.allocator());
+
+ // An exec on the `+` of a hunk line takes the word there; a look, the
+ // address the line names.
+ pane.body.cur_row = 5;
+ pane.body.cur_col = 0;
+ try std.testing.expectEqualStrings("+line", exec.operandAt(p, pane, cursorWordSel(p, pane), false, false).text.?);
+ try std.testing.expectEqualStrings("src/x.zig:1", exec.operandAt(p, pane, cursorWordSel(p, pane), false, true).text.?);
+
+ // An event reader on the pane.
+ const event = pardes.ctlfs.Node.of(pane.serial, .event);
+ const opened = th.call(p, .{ .tag = 1, .op = .open, .node = event });
+ defer _ = th.call(p, .{ .tag = 9, .op = .release, .node = event, .handle = opened.reply.handle });
+ const tag = try tagline.tagText(p, p.scratch.allocator(), pane);
+ const at: u16 = @intCast(std.mem.lastIndexOf(u8, tag, "Del").?);
+ // A B3 click on the tag's Del, where the tag as shown has it...
+ const shown = (try tagline.shownTag(p, p.scratch.allocator(), pane)).text;
+ const shown_at = std.mem.lastIndexOf(u8, shown, "Del").?;
+ const shown_row: u16 = @intCast(std.mem.count(u8, shown[0..shown_at], "\n"));
+ const line_start = if (std.mem.lastIndexOfScalar(u8, shown[0..shown_at], '\n')) |nl| nl + 1 else 0;
+ const x = p.rects[0].x + TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(shown[line_start..shown_at]))) + 1;
+ const y = p.rects[0].y + shown_row;
+ p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x, .row = y } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x, .row = y } });
+ var clicked_buf: [128]u8 = undefined;
+ const clicked = th.rd(p, event, 0, 4096).bytes;
+ @memcpy(clicked_buf[0..clicked.len], clicked);
+ // ...and Enter with the tag's cursor on it.
+ tagline.enterTag(p, pane);
+ pane.tag.mode = .normal;
+ pane.tag.cur_row = 0;
+ pane.tag.cur_col = at + 1;
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ const keyed = th.rd(p, event, 0, 4096).bytes;
+ // The same record but for its origin, mouse or keyboard.
+ try std.testing.expect(clicked.len > 2);
+ try std.testing.expectEqualStrings(clicked_buf[1..clicked.len], keyed[1..]);
+ try std.testing.expect(p.panes[0] != null);
+}
+
test "Look outside a diff pane expands and looks as ever" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
defer p.deinit();
diff --git a/src/mouse.zig b/src/mouse.zig
index 7a2d0805..8020fda8 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -965,6 +965,21 @@ fn reportGesture(
operand: exec.PointerOperand,
chord_arg: ?[]const u8,
) bool {
+ if ((p.panes[id] orelse return false).fs.readers == 0) return false;
+ return reportRange(p, id, cmd, text, on_tag, gestureRange(p, id, text, on_tag, operand), chord_arg);
+}
+
+/// An event reader told of a look or an exec of `text` at `range` of the
+/// tag or body, by a click or a key alike: false when the pane has none.
+pub fn reportRange(
+ p: *Pardes,
+ id: usize,
+ cmd: Builtin,
+ text: []const u8,
+ on_tag: bool,
+ range: ctlfs.pane.State.Range,
+ chord_arg: ?[]const u8,
+) bool {
const scripted = p.panes[id] orelse return false;
if (scripted.fs.readers == 0) return false;
const is_look = cmd == config.look_cmd;
@@ -973,7 +988,6 @@ fn reportGesture(
else
(if (on_tag) .tag_exec else .body_exec);
const named = exec.namesBuiltin(text);
- const range = gestureRange(p, id, text, on_tag, operand);
// No flag 2: the record already carries the word a click took, its
// range and text, and in acme flag 2 promises a second record with them
// (editors/acme/exec.c:154-183, look.c:42-68), which none follows here.
@@ -1293,7 +1307,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
return;
}
};
- var operand = exec.pointerOperand(p, pane, clk);
+ var operand = exec.operandAt(p, pane, clk, true, s.button == config.look_button);
if (operand.expanded) |expanded|
pane.sel[b] = expanded
else if (operand.text == null)
diff --git a/src/pardes.zig b/src/pardes.zig
index 674128a1..425a0993 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -6296,12 +6296,13 @@ pub const Pardes = struct {
const range = t.primaryRange(shown, 0);
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
- const txt = if ((t.vsel.active and t.vsel.explicit) or t.msel.active)
- shown[lo..hi]
+ const span: [2]usize = if ((t.vsel.active and t.vsel.explicit) or t.msel.active)
+ .{ lo, hi }
else word: {
const b = config.wordBounds(shown, modal.selectionCursor(shown, range));
- break :word shown[b.lo..b.hi];
+ break :word .{ b.lo, b.hi };
};
+ const txt = shown[span[0]..span[1]];
// A column tag's command acts on that column, empty or not.
p.exec_column = if (header != null) p.header_column else null;
defer p.exec_column = null;
@@ -6310,7 +6311,12 @@ pub const Pardes = struct {
// Out of the tag or header FIRST: Del frees the pane, Kill
// and Restore the whole session.
if (header != null) tagline.exitHeader(p) else pane.focus = .body;
- if (txt.len > 0) exec.runBuiltin(p, cmd, p.active, "", txt);
+ if (txt.len == 0) return;
+ // A pane's tag word: an event reader hears it as a click on
+ // it, at its place in the tag (flatSurface is the tag's text).
+ if (header == null and t.what == .tag and
+ mouse.reportRange(p, p.active, cmd, txt, true, .{ .q0 = @intCast(span[0]), .q1 = @intCast(span[1]) }, null)) return;
+ exec.runBuiltin(p, cmd, p.active, "", txt);
return;
}
edit.pinPaneCursor(p, &pane.body);
@@ -6330,7 +6336,7 @@ pub const Pardes = struct {
// The cursor's word as a click there takes it: one expansion
// (exec.operandAt), a diff line's address and all, and an
// event reader told as a click tells it.
- const operand = exec.operandAt(p, pane, look.cursorWordSel(p, pane), false);
+ const operand = exec.operandAt(p, pane, look.cursorWordSel(p, pane), false, cmd == config.look_cmd);
mouse.dispatchPointerBuiltin(p, p.active, cmd, operand.text, .{ .on_tag = false, .operand = operand });
return;
}