diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 22:58:16 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | c27222be8ba77a3a6bdda16978f3001510bde055 (patch) | |
| tree | 46e709eba262af80b516ade483d210400a09455f /src/look.zig | |
| parent | 4f65855f372186406f4e48708ec3d5eb9aceb6d3 (diff) | |
| download | pardes-c27222be8ba77a3a6bdda16978f3001510bde055.tar.gz pardes-c27222be8ba77a3a6bdda16978f3001510bde055.zip | |
One diff-aware expansion for a click, a 9P whole-line look and an empty event write-back; a blank context line resolves to its path:line
The mouse expanded a diff line by its kind (a header or a hunk line's
prefix to the line's address, a hunk line's code to words), but an
empty look written back to event used the plain word expansion, so ML
on a `+` took nothing, and a 9P look of a blank context line (` `) was
refused as whitespace. look.diffClick is now the one answer: the
mouse, the whole-line look and the event write-back all ask it, and a
blank context line is a line like any other. A test asks all three on
each kind of line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 98 |
1 files changed, 95 insertions, 3 deletions
diff --git a/src/look.zig b/src/look.zig index 4d23582b..17ec8a1c 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1567,16 +1567,45 @@ pub fn diffAddress(pane: *const Pane, lines: []const []const u8, row: usize, buf return target.address(buf); } +/// What a click at byte `col` of line `row` of a diff pane takes, the one +/// answer for every way a click arrives (a mouse click, a 9P look of the +/// whole line, an empty look or exec written back to `event`): a file or +/// hunk header anywhere, or a hunk line on its `+`/`-`/` ` prefix, is the +/// address the line names (diffAddress); a hunk line's code is words, from +/// `word_from` on, the prefix never one of them. Null when the pane shows +/// no diff or the line is no part of one. +pub const DiffClick = struct { address: ?[]const u8 = null, word_from: usize = 0 }; + +pub fn diffClick(p: *Pardes, pane: *Pane, row: usize, col: usize, buf: []u8) ?DiffClick { + const lines = diffLines(p, pane) orelse return null; + return diffClickIn(pane, lines, row, col, buf); +} + +fn diffClickIn(pane: *const Pane, lines: []const []const u8, row: usize, col: usize, buf: []u8) ?DiffClick { + if (row >= lines.len) return null; + const line = lines[row]; + const address = diffAddress(pane, lines, row, buf) orelse return null; + const target = diff.targetAt(lines, row, pane.file == null) orelse return null; + const prefixed = line.len > 0 and std.mem.indexOfScalar(u8, "+- ", line[0]) != null; + switch (target.kind) { + .context, .added, .removed => if (!(col == 0 and prefixed)) return .{ .word_from = @intFromBool(prefixed) }, + else => {}, + } + return .{ .address = address }; +} + /// A look written over 9P of a whole line of a diff pane (the cursor's line -/// first, then the first after it): its address, as a click there makes. +/// first, then the first after it): what a click on its first column takes. +/// A blank context line (` `) is a line too. fn diffLineAddress(p: *Pardes, pane: *Pane, operand: []const u8, buf: []u8) ?[]const u8 { const text = std.mem.trimEnd(u8, operand, "\r\n"); - if (std.mem.trim(u8, text, " \t").len == 0) return null; + if (text.len == 0) return null; const lines = diffLines(p, pane) orelse return null; const cur: usize = @intCast(@max(0, pane.body.cur_row)); for (0..lines.len) |k| { const i = (cur + k) % lines.len; - if (std.mem.eql(u8, std.mem.trimEnd(u8, lines[i], "\r"), text)) return diffAddress(pane, lines, i, buf); + if (std.mem.eql(u8, std.mem.trimEnd(u8, lines[i], "\r"), text)) + return (diffClickIn(pane, lines, i, 0, buf) orelse return null).address; } return null; } @@ -2230,6 +2259,69 @@ test "Look on a diff: headers and a hunk line's prefix are the whole line, hover } } +test "a click, a 9P whole-line look and an empty event write-back take one expansion on every kind of diff line, a blank context line too" { + if (!platform_has_fs) return; + const diff_text = + \\diff --git a/src/new.zig b/src/new.zig + \\--- a/src/new.zig + \\+++ b/src/new.zig + \\@@ -2,3 +2,3 @@ fn x() void { + \\ two + \\-drei + \\+three + \\ + \\ + ; + var fx: DiffFixture = undefined; + try fx.init(true, &.{.{ "src/new.zig", "one\ntwo\nthree\n\nfive\n" }}, diff_text); + defer fx.tmp.cleanup(); + 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 = false; + const Point = struct { row: u16, col: u16, want: []const u8, whole_line: bool = true }; + const points = [_]Point{ + .{ .row = 0, .col = 0, .want = "src/new.zig" }, // diff --git + .{ .row = 1, .col = 0, .want = "src/new.zig" }, // --- + .{ .row = 2, .col = 0, .want = "src/new.zig" }, // +++ + .{ .row = 3, .col = 0, .want = "src/new.zig:2" }, // @@ + .{ .row = 4, .col = 0, .want = "src/new.zig:2" }, // a context line's ` ` + .{ .row = 5, .col = 0, .want = "src/new.zig:3" }, // `-` + .{ .row = 6, .col = 0, .want = "src/new.zig:3" }, // `+` + .{ .row = 7, .col = 0, .want = "src/new.zig:4" }, // a blank context line + .{ .row = 6, .col = 3, .want = "three", .whole_line = false }, // code: a word + }; + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + for (points) |at| { + p.active = 0; + pane.file.?.scroll = 0; + pane.body.cur_row = 0; + p.sync(); + _ = frame.reset(.retain_capacity); + _ = try p.render(frame.allocator()); + // The mouse's. + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane) + at.col; + const y = p.bodyTop(pane, rect) + at.row; + const pointed = edit.pointerTextSelection(p, 0, x, y, null, null) orelse return error.NothingPointed; + const clicked = exec.pointerOperand(p, pane, pointed.sel).text orelse return error.NothingClicked; + try std.testing.expectEqualStrings(at.want, clicked); + // An empty record written back to event, at the same byte. + const body = pane.file.?.content; + const line_start = modal.lineStartOffset(body, at.row); + const written = pardes.ctlfs.events.expandAt(p, pane, body, line_start + at.col, true); + try std.testing.expectEqualStrings(at.want, written); + // A 9P look of the whole line. + if (at.whole_line) { + const lines = diffLines(p, pane).?; + var buf: [4096 + 32]u8 = undefined; + const whole = diffLineAddress(p, pane, lines[at.row], &buf) orelse return error.NoWholeLineAddress; + try std.testing.expectEqualStrings(at.want, whole); + } + } +} + 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(); |
