summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 21:38:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit988b288b870dd0165f09b991839c2c37fcd3fd62 (patch)
tree6552dc29f322c6ec6ee89b3ffdcc811389305e27 /src/look.zig
parent68dd567218505838814d73473460a0c0ff8e97b3 (diff)
downloadpardes-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.zig273
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);
}