From 1a33ed7221733b181726c7ad01e4ce190bcf1d05 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 17:25:24 -0300 Subject: One Look path: Enter, Tab and a bare Look take what a click at the cursor takes, a diff line's address included, and an event reader is told of them as of a click The keyboard expanded the cursor's word on its own (look.expandedSel), so a diff line's address (and a hunk line's code word, without its +/-) was the mouse's alone: Enter on `+line` looked for the text in the diff. Now exec.operandAt is the one expansion: pointerOperand is it with a gesture's selection borrowed, the Enter/Tab path and a bare Look (exec.currentOperand) are it from the cursor, and both dispatch through mouse.dispatchPointerBuiltin, so a pane with an event reader hears a key's look or exec as a click's. A test runs one diff line and one file:12:5: line through a B3 click, a B3 sweep, Enter, a bare Look, the look file and an event write-back, and asserts each lands in the same file, line and column. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 5 ++- src/exec.zig | 98 +++++++++++++++++++++++++++++++++----------------------- src/look.zig | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/mouse.zig | 4 ++- src/pardes.zig | 8 +++-- 5 files changed, 163 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/builtins.zig b/src/builtins.zig index cca7bfb5..1a2f132f 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -516,7 +516,10 @@ pub const Look = struct { // name on the message row would only be noise over that pub const quiet = true; pub fn run(c: Ctx) void { - look.lookAt(c.p, c.id, c.arg orelse return); + // Bare (a key bound to it, a tag's Look with nothing held): the + // selection, else what Enter on the cursor takes, the one expansion. + const arg = c.arg orelse (exec.currentOperand(c.p, c.pane) orelse return); + look.lookAt(c.p, c.id, arg); } }; diff --git a/src/exec.zig b/src/exec.zig index f3819a83..8ddb9abd 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -108,6 +108,23 @@ pub const PointerOperand = struct { }; pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand { + return operandAt(p, pane, clicked, 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; +} + +/// What a look or an exec at `clicked` takes: the ONE expansion every +/// entry point uses (a B2/B3 click, Enter and Tab in normal mode, a bare +/// 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 { 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; @@ -126,48 +143,49 @@ pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand else clicked.c0; var result: PointerOperand = .{ .row = row, .col = col }; - - const kept = pane.sel[sel_slot]; - const inside_kept = if (pane.pointerSelection(kept)) |rows| inside: { - if (clicked.r0 < pane.tag_rows) break :inside false; - if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false; - const source_col: usize = @intCast(@max(0, col)); - for (rows) |span| { - const target = if (span.raw_terminal) - (if (raw) span.row else pane.surfRow(span.row)) - else - (if (raw) pane.gridRow(span.row) else span.row); - var lo = span.lo; - var hi = span.hi; - if (span.raw_terminal and !raw) { - lo -|= span.prompt_bytes; - hi -|= span.prompt_bytes; - } else if (!span.raw_terminal and raw) { - const prefix = panes.terminal.promptPrefixBytes(pane, row, raw_line); - lo += prefix; - hi += prefix; + if (borrow) { + const kept = pane.sel[sel_slot]; + const inside_kept = if (pane.pointerSelection(kept)) |rows| inside: { + if (clicked.r0 < pane.tag_rows) break :inside false; + if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false; + const source_col: usize = @intCast(@max(0, col)); + for (rows) |span| { + const target = if (span.raw_terminal) + (if (raw) span.row else pane.surfRow(span.row)) + else + (if (raw) pane.gridRow(span.row) else span.row); + var lo = span.lo; + var hi = span.hi; + if (span.raw_terminal and !raw) { + lo -|= span.prompt_bytes; + hi -|= span.prompt_bytes; + } else if (!span.raw_terminal and raw) { + const prefix = panes.terminal.promptPrefixBytes(pane, row, raw_line); + lo += prefix; + hi += prefix; + } + if (target == row and source_col >= lo and source_col < hi) + break :inside true; } - if (target == row and source_col >= lo and source_col < hi) - break :inside true; + break :inside false; + } else edit.spanHas(clicked.r0, clicked.c0, kept.r0, kept.c0, kept.r1, kept.c1); + if (kept.state == .done and inside_kept) { + result.text = edit.selectionText(p, pane, kept) catch null; + result.preview = kept; + return result; + } + if (pane.body.vsel.active and pane.body.vsel.explicit and + edit.spanHas(row, col, pane.body.vsel.row, pane.body.vsel.col, pane.body.cur_row, pane.body.cur_col)) + { + result.text = edit.currentSelText(p, pane); + return result; + } + if (pane.body.msel.active and row >= @min(pane.body.msel.r0, pane.body.msel.r1) and + row <= @max(pane.body.msel.r0, pane.body.msel.r1)) + { + result.text = edit.currentSelText(p, pane); + return result; } - break :inside false; - } else edit.spanHas(clicked.r0, clicked.c0, kept.r0, kept.c0, kept.r1, kept.c1); - if (kept.state == .done and inside_kept) { - result.text = edit.selectionText(p, pane, kept) catch null; - result.preview = kept; - return result; - } - if (pane.body.vsel.active and pane.body.vsel.explicit and - edit.spanHas(row, col, pane.body.vsel.row, pane.body.vsel.col, pane.body.cur_row, pane.body.cur_col)) - { - result.text = edit.currentSelText(p, pane); - return result; - } - if (pane.body.msel.active and row >= @min(pane.body.msel.r0, pane.body.msel.r1) and - row <= @max(pane.body.msel.r0, pane.body.msel.r1)) - { - result.text = edit.currentSelText(p, pane); - return result; } // In a diff, a file or hunk header, and a hunk line pointed at on its // `+`/`-`/` ` prefix, expand to the whole line (the hover shows it) and diff --git a/src/look.zig b/src/look.zig index 115dabe1..a358b31a 100644 --- a/src/look.zig +++ b/src/look.zig @@ -2426,6 +2426,99 @@ test "a click, a 9P whole-line look and an empty event write-back take one expan } } +test "every entry point to Look takes one expansion: a diff line and a file:12:5: line land in one place by each" { + if (!platform_has_fs) return; + const numbered = "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nline 12 here\nl13\n"; + const diff_text = + \\diff --git a/src/x.zig b/src/x.zig + \\--- a/src/x.zig + \\+++ b/src/x.zig + \\@@ -11,2 +11,3 @@ + \\ l11 + \\-old + \\+line 12 here + \\ + ; + const log_text = "src/x.zig:12:5: error: bad\n"; + var fx: DiffFixture = undefined; + try fx.init(true, &.{.{ "src/x.zig", numbered }}, diff_text); + defer fx.tmp.cleanup(); + const th = @import("ninep/testing.zig"); + // `column` -1: wherever a click puts it, every other way the same. + const Case = struct { text: []const u8, name: []const u8, row: u16, col: u16, sweep: [2]u16, line: i32, column: i32 }; + const cases = [_]Case{ + // `+line 12 here` pointed at on its `+`: the hunk's new line 12. + .{ .text = diff_text, .name = "change.diff", .row = 6, .col = 0, .sweep = .{ 0, 12 }, .line = 11, .column = -1 }, + // A compiler's place, its trailing `:` dropped: line 12, column 5. + .{ .text = log_text, .name = "build.log", .row = 0, .col = 2, .sweep = .{ 0, 14 }, .line = 11, .column = 4 }, + }; + for (cases) |case| { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + const pane = try fx.open(p, case.text); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try std.fs.path.join(p.gpa, &.{ fx.dir, case.name }); + pane.tag_expanded = false; + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + const Entry = enum { click, sweep, enter, bare_look, look_file, event_write_back }; + var column = case.column; + for (std.enums.values(Entry)) |entry| { + // From the pane, its cursor on the place, nothing selected, + // the target (once open) moved off it. + for (p.panes[1..]) |slot| if (slot) |other| { + other.body.cur_row = 0; + other.body.cur_col = 0; + }; + p.active = 0; + pane.body.mode = .normal; + pane.body.vsel.active = false; + pane.body.msel.active = false; + pane.body.cur_row = case.row; + pane.body.cur_col = case.col; + pane.body.cur_pinned = true; + pane.file.?.scroll = 0; + p.sync(); + _ = frame.reset(.retain_capacity); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x0 = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect) + case.row; + switch (entry) { + .click => { + p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x0 + case.col, .row = y } }); + p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x0 + case.col, .row = y } }); + }, + .sweep => { + p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x0 + case.sweep[0], .row = y } }); + p.update(.{ .mouse = .{ .button = .right, .kind = .drag, .col = x0 + case.sweep[1], .row = y } }); + p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x0 + case.sweep[1], .row = y } }); + }, + .enter => p.update(.{ .key = .{ .cp = Key.enter } }), + .bare_look => _ = p.executeBuiltinLine(0, "Look"), + .look_file => { + const line = modal.lineSlice(case.text, case.row); + // A diff line written whole; a place, as a script writes it. + const written = if (std.mem.eql(u8, case.name, "change.diff")) line else line[case.sweep[0] .. case.sweep[1] + 1]; + _ = th.wr(p, @intFromEnum(pardes.ctlfs.TopFile.look), written); + }, + .event_write_back => { + const body = pane.file.?.content; + const text = pardes.ctlfs.events.expandAt(p, pane, body, modal.lineStartOffset(body, case.row) + case.col, true); + lookAt(p, 0, p.scratch.allocator().dupe(u8, text) catch unreachable); + }, + } + const landed = p.panes[p.active].?; + const f = landed.file orelse return error.NothingOpened; + if (column < 0) column = landed.body.cur_col; + if (!std.mem.endsWith(u8, f.path, "/src/x.zig") or landed.body.cur_row != case.line or landed.body.cur_col != column) { + std.debug.print("{s} by {s}: at {s} {d}:{d}\n", .{ case.name, @tagName(entry), f.path, landed.body.cur_row, landed.body.cur_col }); + return error.EntryPointsDiffer; + } + } + } +} + 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 21961cf8..7a2d0805 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -1044,7 +1044,9 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: return .{ .q0 = q0, .q1 = @intCast(@min(q0 + text.len, f.content.len)) }; } -fn dispatchPointerBuiltin( +/// A look or exec of what an operand took (exec.operandAt), from a +/// click or a key: an event reader is told, else it runs. +pub fn dispatchPointerBuiltin( p: *Pardes, id: usize, cmd: Builtin, diff --git a/src/pardes.zig b/src/pardes.zig index d0bb9229..1cc1c5b8 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -6326,9 +6326,11 @@ pub const Pardes = struct { return; } } - const sel = look.expandedSel(p, pane, look.cursorWordSel(p, pane)) orelse return; - const word = edit.selectionText(p, pane, sel) catch return; - exec.runBuiltinFrom(p, cmd, p.active, "", word, true); + // 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); + mouse.dispatchPointerBuiltin(p, p.active, cmd, operand.text, .{ .on_tag = false, .operand = operand }); return; } if (t.mode == .normal and t.what == .body and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key); -- cgit v1.3