diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 21:38:57 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 988b288b870dd0165f09b991839c2c37fcd3fd62 (patch) | |
| tree | 6552dc29f322c6ec6ee89b3ffdcc811389305e27 /src/look.zig | |
| parent | 68dd567218505838814d73473460a0c0ff8e97b3 (diff) | |
| download | pardes-988b288b870dd0165f09b991839c2c37fcd3fd62.tar.gz pardes-988b288b870dd0165f09b991839c2c37fcd3fd62.zip | |
A diff line looks up the path and line it names, as any look does
The look on a diff line was its own resolver: a walk up to .git/.jj from
the diff's directory, its own "no such file here" and "deleted by this
diff". A review tool's cached patch, in no repository, missed every file
though the session's terminals sat in the checkout.
Now a diff line only expands and transforms. Its expansion (what the hover
shows) is the whole line for a header, a `@@` line, or a hunk line
pointed at on its prefix, and its text is the look address the line names
(look.diffAddress): `path` for `diff --git`/`---`/`+++`, `path:c` for
`@@ -a,b +c,d @@`, `path:N` for a hunk line's new line (a removed line's,
the new line now where it was). That text goes through the regular look
exactly as if selected by hand: same resolution, placement and errors. A
9P look of a whole diff line takes the same transform.
Kept: the line classification, hunk arithmetic, git prefix stripping and
timestamp cutting (diff.zig). Gone: the repo-root walk, its messages,
Target.old_side and deletesAhead, and the click's row stash
(Pardes.diff_look). A test compares a B3 on a cached patch's line with a
look of the hand-selected `entry/src/entry.rs:2` in the same session.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 273 |
1 files changed, 104 insertions, 169 deletions
diff --git a/src/look.zig b/src/look.zig index 36b91545..3cd1faa5 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1539,82 +1539,26 @@ pub fn diffLines(p: *Pardes, pane: *Pane) ?[]const []const u8 { return p.diff_output.lines(p, pane); } -const DiffLook = union(enum) { none, done, at: []const u8 }; - -/// A look in a diff pane at one of its lines opens the file and line that -/// line names (diff.targetAt): the new file at a `@@` line's first new -/// line, at a context or added line's own line, and at a removed line's -/// place, the new line now standing where it was. `---`/`+++` open their -/// file. The path is the repository's name (diff.Walk.side: git's side -/// prefixes dropped by git's rules, a plain diff's names as written), -/// found from the repository root (walking up from the diff's directory to -/// a `.git` or `.jj`), else the directory. A file that is not there opens -/// nothing and says so, by that name. -/// -/// Which line: the one a click just gave (`clicked`: exec.pointerOperand -/// expanded to it and kept its row), else, for a look written over 9P, a -/// line the text is the whole of, the cursor's first, then the first after -/// it. A word on a hunk line is looked at as ever. -fn diffLook(p: *Pardes, id: usize, pane: *Pane, operand: []const u8, clicked: bool, buf: []u8) DiffLook { - const text = std.mem.trimEnd(u8, operand, "\r\n"); - if (std.mem.trim(u8, text, " \t").len == 0) return .none; - const lines = diffLines(p, pane) orelse return .none; - const row: usize = row: { - if (p.diff_look) |at| if (at.serial == pane.serial and at.row < lines.len and - std.mem.eql(u8, std.mem.trimEnd(u8, lines[at.row], "\r"), text)) - { - p.diff_look = null; - break :row at.row; - }; - if (clicked) return .none; - 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)) break :row i; - } - return .none; - }; - const target = diff.targetAt(lines, row, pane.file == null) orelse return .none; - const base = if (pane.file) |f| std.fs.path.dirname(f.path) orelse "/" else pane.cwdSlice(); - var realbuf: [4096]u8 = undefined; - const found = diffFile(base, target.path, &realbuf) orelse { - var cut: [miss_path_cap + "…".len]u8 = undefined; - if (target.old_side) - missed(p, id, "{s}: deleted by this diff", .{missText(target.path, &cut)}) - else - missed(p, id, "{s}: no such file here", .{missText(target.path, &cut)}); - return .done; - }; - const place = if (target.line > 0) - std.fmt.bufPrint(buf, "{s}:{d}", .{ found, target.line }) catch return .none - else - std.fmt.bufPrint(buf, "{s}", .{found}) catch return .none; - return .{ .at = place }; +/// The look address a diff line names (diff.targetAt): `path` for a +/// `diff --git`, `---` or `+++` line, `path:N` for a `@@` line (its first +/// new line) and a hunk line (its new line; a removed line's, the new line +/// now standing where it was). The look of it is a regular one, as of that +/// text selected by hand. Null for a line no part of a file section. +pub fn diffAddress(pane: *const Pane, lines: []const []const u8, row: usize, buf: []u8) ?[]const u8 { + const target = diff.targetAt(lines, row, pane.file == null) orelse return null; + return target.address(buf); } -/// The file a diff names, from `base` (the diff's directory): tried in the -/// repository `base` is in, then in `base`. -fn diffFile(base: []const u8, path: []const u8, out: *[4096]u8) ?[]const u8 { - if (comptime !platform_has_fs) return null; - if (path.len == 0) return null; - if (path[0] == '/') { - const found = fs.resolveOs(path, out) orelse return null; - return if (found.dir) null else found.path; - } - var root_buf: [4096]u8 = undefined; - var dir: ?[]const u8 = base; - const root: ?[]const u8 = while (dir) |d| : (dir = std.fs.path.dirname(d)) { - var probe: [4096]u8 = undefined; - const git = std.fmt.bufPrint(&probe, "{s}/.git", .{d}) catch break null; - if (fs.resolveOs(git, &root_buf) != null) break d; - const jj = std.fmt.bufPrint(&probe, "{s}/.jj", .{d}) catch break null; - if (fs.resolveOs(jj, &root_buf) != null) break d; - } else null; - for ([_]?[]const u8{ root, base }) |maybe| { - const at = maybe orelse continue; - var joined: [4096]u8 = undefined; - const full = std.fmt.bufPrint(&joined, "{s}/{s}", .{ at, path }) catch continue; - if (fs.resolveOs(full, out)) |found| if (!found.dir) return found.path; +/// 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. +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; + 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); } return null; } @@ -1639,24 +1583,15 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void { p.noteHaptic(.look); noteLookSource(p, id); const command = std.mem.trim(u8, operand, " \t\r\n"); - // A click on a diff line (a context line ` Del` included) is a look at - // the place it names, never the word run. + // A whole line of a diff written over 9P (a context line ` Del` + // included) is a look at the address it names, never the word run. var diff_buf: [4096 + 32]u8 = undefined; - const looked: []const u8 = looked: { - switch (diffLook(p, id, pane, operand_raw, true, &diff_buf)) { - .none => {}, - .done => return, - .at => |place| break :looked place, - } + const looked: []const u8 = diffLineAddress(p, pane, operand_raw, &diff_buf) orelse looked: { if (config.commandWord(command)) |cmd| { if (exec.execute(p, id, cmd)) |dst| p.active = dst; return; } - switch (diffLook(p, id, pane, operand_raw, false, &diff_buf)) { - .none => break :looked operand, - .done => return, - .at => |place| break :looked place, - } + break :looked operand; }; // The operand may borrow selected terminal/PDF text released below. const txt = p.scratch.allocator().dupe(u8, looked) catch return; @@ -2084,13 +2019,14 @@ const DiffFixture = struct { return pane; } - /// A right click's look at `row` of the diff pane (slot 0). + /// A right click's look at `row` of the diff pane (slot 0): the look + /// of the address that line names. fn click(p: *Pardes, diff_pane: *Pane, row: usize) void { p.active = 0; const lines = diffLines(p, diff_pane).?; - p.diff_look = .{ .serial = diff_pane.serial, .row = row }; + var buf: [4096]u8 = undefined; p.look_missed = false; - lookAt(p, 0, lines[row]); + lookAt(p, 0, diffAddress(diff_pane, lines, row, &buf).?); } }; @@ -2149,11 +2085,9 @@ test "Look on each kind of diff line opens the new file at the line it names" { try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/src/a.zig")); try std.testing.expect(!p.look_missed); - // No click: a 9P look writes a line, found after the cursor, or the - // text is on the cursor's line. + // No click: a 9P look writes a whole line, found from the cursor on. p.active = 0; pane.body.cur_row = 0; - p.diff_look = null; lookAt(p, 0, "+line 12"); try expectAt(p, "/src/a.zig", 12); // A word on a hunk line is a word, looked for in the diff as ever. @@ -2168,43 +2102,6 @@ test "Look on each kind of diff line opens the new file at the line it names" { try std.testing.expectEqual(@as(usize, 0), p.active); } -test "Look on a diff line of a file that is not here says so and opens nothing" { - if (!platform_has_fs) return; - const diff_text = - \\diff --git a/src/elsewhere.zig b/src/elsewhere.zig - \\--- a/src/elsewhere.zig - \\+++ b/src/elsewhere.zig - \\@@ -1 +1 @@ - \\-const a = 1; - \\+const a = 2; - \\diff --git a/old.zig b/old.zig - \\deleted file mode 100644 - \\--- a/old.zig - \\+++ /dev/null - \\@@ -1 +0,0 @@ - \\-const gone = 1; - \\ - ; - var fx: DiffFixture = undefined; - try fx.init(true, &.{}, 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); - var before: usize = 0; - for (p.panes) |slot| before += @intFromBool(slot != null); - DiffFixture.click(p, pane, 5); - try std.testing.expect(p.look_missed); - try std.testing.expectEqual(@as(usize, 0), p.active); - var after: usize = 0; - for (p.panes) |slot| after += @intFromBool(slot != null); - try std.testing.expectEqual(before, after); - try std.testing.expectEqualStrings("Look: src/elsewhere.zig: no such file here", pane.msg[0..pane.msg_len]); - DiffFixture.click(p, pane, 11); - try std.testing.expect(p.look_missed); - try std.testing.expectEqualStrings("Look: old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); -} - test "Look on plain diff -u output: names as written, timestamps cut, next to the diff" { if (!platform_has_fs) return; const diff_text = "--- old/x.zig\t2026-09-30 10:00:00.000000000 +0000\n" ++ @@ -2250,22 +2147,22 @@ test "Look on a diff: headers and a hunk line's prefix are the whole line, hover pane.tag_expanded = false; // `word`: what the pointer there expands to when it is not the whole // line; `line` 0: that word is looked at in the diff, nothing opened. - const Point = struct { row: u16, col: u16, line: usize, word: ?[]const u8 = null }; + const Point = struct { row: u16, col: u16, line: usize, word: ?[]const u8 = null, addr: []const u8 = "" }; const points = [_]Point{ - .{ .row = 0, .col = 0, .line = 1 }, // `diff`: the new side - .{ .row = 0, .col = 35, .line = 1 }, // on b/src/new.zig - .{ .row = 4, .col = 1, .line = 1 }, // `---`: renamed, so the new name - .{ .row = 4, .col = 5, .line = 1 }, // the `a/` of the old name - .{ .row = 5, .col = 0, .line = 1 }, // `+++` - .{ .row = 5, .col = 4, .line = 1 }, // its `b/` - .{ .row = 5, .col = 10, .line = 1 }, // its path - .{ .row = 6, .col = 0, .line = 2 }, // the first `@@` - .{ .row = 6, .col = 9, .line = 2 }, // `+2,2` - .{ .row = 6, .col = 13, .line = 2 }, // the second `@@` - .{ .row = 6, .col = 20, .line = 2 }, // the function after it - .{ .row = 7, .col = 0, .line = 2 }, // a context line's ` ` - .{ .row = 8, .col = 0, .line = 3 }, // a removed line's `-`: where it was - .{ .row = 9, .col = 0, .line = 3 }, // an added line's `+` + .{ .row = 0, .col = 0, .line = 1, .addr = "src/new.zig" }, // `diff`: the new side + .{ .row = 0, .col = 35, .line = 1, .addr = "src/new.zig" }, // on b/src/new.zig + .{ .row = 4, .col = 1, .line = 1, .addr = "src/new.zig" }, // `---`: renamed, so the new name + .{ .row = 4, .col = 5, .line = 1, .addr = "src/new.zig" }, // the `a/` of the old name + .{ .row = 5, .col = 0, .line = 1, .addr = "src/new.zig" }, // `+++` + .{ .row = 5, .col = 4, .line = 1, .addr = "src/new.zig" }, // its `b/` + .{ .row = 5, .col = 10, .line = 1, .addr = "src/new.zig" }, // its path + .{ .row = 6, .col = 0, .line = 2, .addr = "src/new.zig:2" }, // the first `@@` + .{ .row = 6, .col = 9, .line = 2, .addr = "src/new.zig:2" }, // `+2,2` + .{ .row = 6, .col = 13, .line = 2, .addr = "src/new.zig:2" }, // the second `@@` + .{ .row = 6, .col = 20, .line = 2, .addr = "src/new.zig:2" }, // the function after it + .{ .row = 7, .col = 0, .line = 2, .addr = "src/new.zig:2" }, // a context line's ` ` + .{ .row = 8, .col = 0, .line = 3, .addr = "src/new.zig:3" }, // a removed line's `-`: where it was + .{ .row = 9, .col = 0, .line = 3, .addr = "src/new.zig:3" }, // an added line's `+` .{ .row = 9, .col = 3, .line = 0, .word = "three" }, // its code: a word .{ .row = 7, .col = 2, .line = 0, .word = "two" }, }; @@ -2291,7 +2188,8 @@ test "Look on a diff: headers and a hunk line's prefix are the whole line, hover try std.testing.expectEqualStrings(word, operand.text.?); try std.testing.expectEqualStrings(word, lines[at.row][@intCast(span.lo)..@intCast(span.hi)]); } else { - try std.testing.expectEqualStrings(lines[at.row], operand.text.?); + // The hover shows the line; the look is of the address it names. + try std.testing.expectEqualStrings(at.addr, operand.text.?); try std.testing.expectEqual(@as(i32, 0), span.lo); try std.testing.expectEqual(@as(i32, @intCast(lines[at.row].len)), span.hi); } @@ -2355,28 +2253,65 @@ test "Look on a diff under each git prefix opens the repository's file, and a re } } -test "Look on a diff says a missing or deleted file by its repository name, a plain diff's as written" { + +test "Look on a diff line is the look of the address it names, selected by hand: a cached patch, the checkout elsewhere" { if (!platform_has_fs) return; - const git_text = "diff --git c/src/gone.zig w/src/gone.zig\n--- c/src/gone.zig\n+++ w/src/gone.zig\n@@ -1 +1 @@\n-a\n+b\n" ++ - "diff --git a/lib/old.zig b/lib/old.zig\ndeleted file mode 100644\n--- a/lib/old.zig\n+++ /dev/null\n@@ -1 +0,0 @@\n-a\n"; - const plain_text = "--- a/x.zig\t2026-09-30 10:00:00 +0000\n+++ b/x.zig\t2026-09-30 11:00:00 +0000\n@@ -1 +1 @@\n-a\n+b\n"; - var fx: DiffFixture = undefined; - try fx.init(true, &.{.{ "x.zig", "b\n" }}, git_text); - defer fx.tmp.cleanup(); - const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); - defer p.deinit(); - var pane = try fx.open(p, git_text); - DiffFixture.click(p, pane, 5); - try std.testing.expect(p.look_missed); - try std.testing.expectEqualStrings("Look: src/gone.zig: no such file here", pane.msg[0..pane.msg_len]); - DiffFixture.click(p, pane, 11); - try std.testing.expectEqualStrings("Look: lib/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); - DiffFixture.click(p, pane, 6); // `diff --git` of the deleted file - try std.testing.expectEqualStrings("Look: lib/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]); - // A plain diff's `b/x.zig` is a path as written: there is no b/ here, - // and the x.zig at the top is not it. - pane = try fx.open(p, plain_text); - DiffFixture.click(p, pane, 4); - try std.testing.expect(p.look_missed); - try std.testing.expectEqualStrings("Look: b/x.zig: no such file here", pane.msg[0..pane.msg_len]); + const io = std.testing.io; + const patch = "diff --git a/entry/src/entry.rs b/entry/src/entry.rs\n--- a/entry/src/entry.rs\n+++ b/entry/src/entry.rs\n" ++ + "@@ -1,2 +1,2 @@\n-fn old() {}\n+fn new() {}\n fn kept() {}\n"; + // The review tool's cache, with no checkout in it. + var cache: DiffFixture = undefined; + try cache.init(false, &.{}, patch); + defer cache.tmp.cleanup(); + // The checkout, elsewhere, with a file of it open in the session. + var checkout = std.testing.tmpDir(.{}); + defer checkout.cleanup(); + try checkout.dir.createDir(io, ".git", .default_dir); + try checkout.dir.createDirPath(io, "entry/src"); + try checkout.dir.writeFile(io, .{ .sub_path = "entry/src/entry.rs", .data = "fn new() {}\nfn kept() {}\n" }); + try checkout.dir.writeFile(io, .{ .sub_path = "Cargo.toml", .data = "[workspace]\n" }); + var path_buf: [4096]u8 = undefined; + const cargo = path_buf[0..try checkout.dir.realPathFile(io, "Cargo.toml", &path_buf)]; + + const Seen = struct { active_path: []u8, row: i32, missed: bool, msg: []u8, panes: usize }; + var seen: [2]Seen = undefined; + for (0..2) |way| { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + const pane = try cache.open(p, patch); + // The checkout's file, opened by a look from the diff pane. + lookAt(p, 0, cargo); + p.update(.tick); + try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "Cargo.toml")); + p.active = 0; + p.update(.tick); + p.look_missed = false; + if (way == 0) { + DiffFixture.click(p, pane, 6); // ` fn kept() {}`, new line 2 + } else { + lookAt(p, 0, "entry/src/entry.rs:2"); // the same, selected by hand + } + const shown = p.panes[p.active].?; + var n: usize = 0; + for (p.panes) |slot| n += @intFromBool(slot != null); + seen[way] = .{ + .active_path = try std.testing.allocator.dupe(u8, if (shown.file) |f| f.path else ""), + .row = shown.body.cur_row, + .missed = p.look_missed, + .msg = try std.testing.allocator.dupe(u8, pane.msg[0..pane.msg_len]), + .panes = n, + }; + } + defer for (seen) |s| { + std.testing.allocator.free(s.active_path); + std.testing.allocator.free(s.msg); + }; + try std.testing.expectEqualStrings(seen[1].active_path, seen[0].active_path); + try std.testing.expectEqual(seen[1].row, seen[0].row); + try std.testing.expectEqual(seen[1].missed, seen[0].missed); + try std.testing.expectEqualStrings(seen[1].msg, seen[0].msg); + try std.testing.expectEqual(seen[1].panes, seen[0].panes); + // And it works: the checkout's file, at the line. + try std.testing.expect(std.mem.endsWith(u8, seen[0].active_path, "/entry/src/entry.rs")); + try std.testing.expectEqual(@as(i32, 1), seen[0].row); } |
