summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/tags.md20
-rw-r--r--src/CHANGELOG.md8
-rw-r--r--src/diff.zig208
-rw-r--r--src/look.zig76
4 files changed, 236 insertions, 76 deletions
diff --git a/docs/tags.md b/docs/tags.md
index 51977390..ec454bb8 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -97,13 +97,19 @@ opens the new file at line c. On a hunk line, the `+`, `-` or space in its
first column takes the whole line and opens the new file at that line; for
a removed line, at the new line now standing where it was (the one after
it, or the hunk's last when it went from the end of the file). The code
-after it is words, looked at as ever. Paths are the repository's: pardes
-walks up from the diff's directory (a command pane's own) to a `.git` or
-`.jj` and drops git's `a/`/`b/`, else takes them from that directory as
-written; `diff -u`'s timestamps are cut. A file that is not here (a branch
-not checked out) opens nothing and says so: `Look: b/x.zig: no such file
-here`, or `deleted by this diff` for a deleted one. A 9P `look` of a whole
-line of the diff does the same.
+after it is words, looked at as ever.
+
+A path is the repository's name for the file. In a git section (one with a
+`diff --git` line) git's side prefix is dropped when both paths on that
+line carry one, different ones: `a/`/`b/`, or with `diff.mnemonicPrefix`
+`c/`, `i/`, `w/`, `o/`. `--no-prefix` writes none, so there `a/x a/x` is a
+real directory `a`, kept. A plain `diff -u`'s names are used as written,
+past the timestamp. The file is looked for from the repository's root
+(pardes walks up from the diff's directory, a command pane's own, to a
+`.git` or `.jj`), else from that directory. A file that is not here (a
+branch not checked out) opens nothing and says so by that name: `Look:
+src/x.zig: no such file here`, or `deleted by this diff` for a deleted one.
+A 9P `look` of a whole line of the diff does the same.
## Editing tags
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 881df10c..69b737ca 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## Unreleased
+
+- A look on a diff names a file as the repository does: git's `a/`/`b/`
+ (or mnemonic `c/`, `i/`, `w/`) is dropped only where both paths of the
+ `diff --git` line carry one, a `--no-prefix` diff's real `a/` is kept, a
+ plain `diff -u`'s names are as written, and a missing file is said by
+ that name (`Look: src/x.zig: no such file here`).
+
## 0.0.10
- A 9P write never moves dot's end: scrolling written text into view moves
diff --git a/src/diff.zig b/src/diff.zig
index 4962cddc..e2b4afd4 100644
--- a/src/diff.zig
+++ b/src/diff.zig
@@ -39,13 +39,17 @@ pub const Line = struct {
};
pub const Walk = struct {
- /// The section's paths, as written, borrowed from the lines walked:
- /// `a/` and `b/` kept, timestamps cut.
+ /// The section's paths, borrowed from the lines walked: timestamps
+ /// cut, and in a git section git's own side prefix (`strip`) dropped,
+ /// so they are the repository's names.
old_path: []const u8 = "",
new_path: []const u8 = "",
- /// `diff --git a/x b/y`'s b side, for a section with no `+++` yet
+ /// `diff --git a/x b/y`'s new side, for a section with no `+++` yet
/// (a mode change, a rename, a binary file).
git_path: []const u8 = "",
+ /// The section's `diff --git` line has git's side prefixes on both its
+ /// paths (gitHeader), so its `---`/`+++` paths have them too.
+ strip: bool = false,
old_left: usize = 0,
new_left: usize = 0,
old_at: usize = 0,
@@ -75,6 +79,13 @@ pub const Walk = struct {
return isDevNull(w.new_path);
}
+ /// A `---`/`+++` path as the repository names it: git's side prefix
+ /// dropped in a section whose `diff --git` line has them.
+ pub fn side(w: *const Walk, path_raw: []const u8) []const u8 {
+ if (w.strip and !isDevNull(path_raw)) return prefixed(path_raw) orelse path_raw;
+ return path_raw;
+ }
+
pub fn step(w: *Walk, line_raw: []const u8) Line {
const line = std.mem.trimEnd(u8, line_raw, "\r");
const got = w.stepInner(line);
@@ -120,18 +131,21 @@ pub const Walk = struct {
return .{ .kind = .no_newline, .old = w.old_at, .new = w.new_at };
if (std.mem.startsWith(u8, line, "diff ")) {
w.* = .{ .continued_rows = w.continued_rows };
- w.git_path = gitPath(line);
+ if (gitHeader(line)) |h| {
+ w.git_path = h.new;
+ w.strip = h.strip;
+ }
return .{ .kind = .meta };
}
if (std.mem.startsWith(u8, line, "--- ")) {
// A second `---` with no `+++` between starts another section
// (plain `diff -u` output has no `diff` line of its own).
- w.* = .{ .continued_rows = w.continued_rows, .git_path = w.git_path };
- w.old_path = cutPath(line[4..]);
+ w.* = .{ .continued_rows = w.continued_rows, .git_path = w.git_path, .strip = w.strip };
+ w.old_path = w.side(cutPath(line[4..]));
return .{ .kind = .old_path };
}
if (std.mem.startsWith(u8, line, "+++ ")) {
- w.new_path = cutPath(line[4..]);
+ w.new_path = w.side(cutPath(line[4..]));
return .{ .kind = .new_path };
}
if (parseHunk(line)) |h| {
@@ -196,13 +210,50 @@ fn isStamp(t: []const u8) bool {
return false;
}
-/// `diff --git a/x b/y`: the b side. A name with a space in it is split at
-/// the last ` b/`, which is right unless the old name has one too.
-fn gitPath(line: []const u8) []const u8 {
- if (!std.mem.startsWith(u8, line, "diff --git ")) return "";
+/// The one-letter side prefixes git writes: `a/`/`b/`, and with
+/// `diff.mnemonicPrefix` `c/` (commit), `i/` (index), `w/` (work tree) and
+/// `o/` (object).
+const side_letters = "abciow";
+
+/// `path` past a side prefix, or null when it has none.
+fn prefixed(path: []const u8) ?[]const u8 {
+ if (path.len > 2 and path[1] == '/' and std.mem.indexOfScalar(u8, side_letters, path[0]) != null) return path[2..];
+ return null;
+}
+
+pub const GitHeader = struct {
+ /// The new side, its prefix dropped when `strip`.
+ new: []const u8,
+ /// Both paths carry a side prefix, and different ones, as git writes
+ /// them (`a/x b/x`, `c/x w/x`, `i/x w/x`). `--no-prefix` writes none,
+ /// so `a/x a/x` there is a real directory `a`, kept.
+ strip: bool,
+};
+
+/// `diff --git <old> <new>`, split where both sides are prefixed (the
+/// names equal, else the last such split), else where the halves are
+/// equal (`--no-prefix`), else at the last blank. A name with a blank in
+/// it splits wrongly only in a rename, and only on a matching prefix.
+pub fn gitHeader(line_raw: []const u8) ?GitHeader {
+ const line = std.mem.trimEnd(u8, line_raw, "\r ");
+ if (!std.mem.startsWith(u8, line, "diff --git ")) return null;
const rest = line["diff --git ".len..];
- const at = std.mem.lastIndexOf(u8, rest, " b/") orelse return "";
- return std.mem.trimEnd(u8, rest[at + 1 ..], " \r");
+ var split: ?usize = null;
+ var at: usize = 0;
+ while (std.mem.indexOfScalarPos(u8, rest, at, ' ')) |blank| : (at = blank + 1) {
+ const old = rest[0..blank];
+ const new = rest[blank + 1 ..];
+ const o = prefixed(old) orelse continue;
+ const n = prefixed(new) orelse continue;
+ if (old[0] == new[0]) continue;
+ split = blank;
+ if (std.mem.eql(u8, o, n)) break;
+ }
+ if (split) |blank| return .{ .new = rest[blank + 3 ..], .strip = true };
+ if (rest.len % 2 == 1 and rest[rest.len / 2] == ' ' and std.mem.eql(u8, rest[0 .. rest.len / 2], rest[rest.len / 2 + 1 ..]))
+ return .{ .new = rest[rest.len / 2 + 1 ..], .strip = false };
+ const blank = std.mem.lastIndexOfScalar(u8, rest, ' ') orelse return .{ .new = rest, .strip = false };
+ return .{ .new = rest[blank + 1 ..], .strip = false };
}
pub const HunkHeader = struct { old_start: usize, old_count: usize, new_start: usize, new_count: usize };
@@ -276,7 +327,7 @@ pub fn looksLikeDiff(lines: []const []const u8) bool {
pub const Target = struct {
/// The line's own kind.
kind: Kind,
- /// As written in the diff, `a/`/`b/` kept.
+ /// The repository's name in a git section (Walk.side), else as written.
path: []const u8,
/// From 1; 0 for the file alone (a `---`/`+++` line).
line: usize,
@@ -284,6 +335,19 @@ pub const Target = struct {
old_side: bool = false,
};
+/// Whether the section `row` is in deletes its file by a line still ahead
+/// of it (`deleted file mode`, `+++ /dev/null`): its `diff --git` and `---`
+/// lines come first, and are about a file this diff deletes all the same.
+fn deletesAhead(lines: []const []const u8, row: usize) bool {
+ for (lines[row + 1 ..]) |raw| {
+ const l = std.mem.trimEnd(u8, raw, "\r");
+ if (std.mem.startsWith(u8, l, "diff ") or std.mem.startsWith(u8, l, "@@")) return false;
+ if (std.mem.startsWith(u8, l, "deleted file mode") or std.mem.eql(u8, l, "+++ /dev/null") or
+ (std.mem.startsWith(u8, l, "+++ /dev/null") and l.len > 13 and (l[13] == '\t' or l[13] == ' '))) return true;
+ }
+ return false;
+}
+
/// What a look at `lines[row]` opens, or null for a line that is no part
/// of a file section (a commit message, `index ...`). The whole line is
/// the look's: a click on a `+++`, `---` or `@@` marker is one on its path
@@ -299,19 +363,6 @@ pub const Target = struct {
/// from the end of the file. In a section that deletes its file there is
/// no new file, so a removed or context line opens the old one at its old
/// line (it is there while the diff is not applied).
-/// Whether the section `row` is in deletes its file by a line still ahead
-/// of it (`deleted file mode`, `+++ /dev/null`): its `diff --git` and `---`
-/// lines come first, and are about a file this diff deletes all the same.
-fn deletesAhead(lines: []const []const u8, row: usize) bool {
- for (lines[row + 1 ..]) |raw| {
- const l = std.mem.trimEnd(u8, raw, "\r");
- if (std.mem.startsWith(u8, l, "diff ") or std.mem.startsWith(u8, l, "@@")) return false;
- if (std.mem.startsWith(u8, l, "deleted file mode") or std.mem.eql(u8, l, "+++ /dev/null") or
- (std.mem.startsWith(u8, l, "+++ /dev/null") and l.len > 13 and (l[13] == '\t' or l[13] == ' '))) return true;
- }
- return false;
-}
-
pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Target {
if (row >= lines.len) return null;
var w: Walk = .{ .continued_rows = continued_rows };
@@ -327,14 +378,14 @@ pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Ta
return null;
},
.old_path, .new_path => {
- const own = cutPath(line[4..]);
+ const own = w.side(cutPath(line[4..]));
// `---` names the old file, but the new one on the `+++` under
// it wins when they differ: a new file (`--- /dev/null`), a
// rename, `diff -u old/x new/x`. The old name is the one
// likely gone.
if (got.kind == .old_path and row + 1 < lines.len and std.mem.startsWith(u8, lines[row + 1], "+++ ")) {
- const new = cutPath(std.mem.trimEnd(u8, lines[row + 1], "\r")[4..]);
- if (!isDevNull(new) and (isDevNull(own) or !std.mem.eql(u8, stripSide(new), stripSide(own))))
+ const new = w.side(cutPath(std.mem.trimEnd(u8, lines[row + 1], "\r")[4..]));
+ if (!isDevNull(new) and (isDevNull(own) or !std.mem.eql(u8, new, own)))
return .{ .kind = got.kind, .path = new, .line = 0 };
}
if (!isDevNull(own)) return .{ .kind = got.kind, .path = own, .line = 0, .old_side = got.kind == .old_path and (w.deleted() or deletesAhead(lines, row)) };
@@ -358,12 +409,6 @@ pub fn targetAt(lines: []const []const u8, row: usize, continued_rows: bool) ?Ta
}
}
-/// `a/x` and `b/x` as git writes them: the name in the repository.
-pub fn stripSide(path: []const u8) []const u8 {
- if (path.len > 2 and (path[0] == 'a' or path[0] == 'b') and path[1] == '/') return path[2..];
- return path;
-}
-
// ---- tests ----
const testing = std.testing;
@@ -408,7 +453,7 @@ test "diff every line of a section that deletes its file is about a file this di
for (lines, 0..) |line, row| {
if (row < 15) continue; // the deleting section: `diff --git a/old.py` on
const t = targetAt(&lines, row, false) orelse continue;
- try testing.expectEqualStrings("old.py", stripSide(t.path));
+ try testing.expectEqualStrings("old.py", t.path);
try testing.expect(t.old_side);
_ = line;
rows += 1;
@@ -440,25 +485,25 @@ test "diff a look on each kind of line names the new file and its line" {
try testing.expectEqual(line, t.line);
}
};
- try expect.at(&lines, 0, "b/src/a.zig", 0); // diff --git
+ try expect.at(&lines, 0, "src/a.zig", 0); // diff --git
try testing.expect(targetAt(&lines, 1, false) == null); // index
- try expect.at(&lines, 2, "a/src/a.zig", 0); // ---
- try expect.at(&lines, 3, "b/src/a.zig", 0); // +++
- try expect.at(&lines, 4, "b/src/a.zig", 10); // @@
- try expect.at(&lines, 5, "b/src/a.zig", 10); // context
- try expect.at(&lines, 6, "b/src/a.zig", 11); // removed: the line now there
- try expect.at(&lines, 7, "b/src/a.zig", 11); // added
- try expect.at(&lines, 8, "b/src/a.zig", 12);
- try expect.at(&lines, 9, "b/src/a.zig", 13);
+ try expect.at(&lines, 2, "src/a.zig", 0); // ---
+ try expect.at(&lines, 3, "src/a.zig", 0); // +++
+ try expect.at(&lines, 4, "src/a.zig", 10); // @@
+ try expect.at(&lines, 5, "src/a.zig", 10); // context
+ try expect.at(&lines, 6, "src/a.zig", 11); // removed: the line now there
+ try expect.at(&lines, 7, "src/a.zig", 11); // added
+ try expect.at(&lines, 8, "src/a.zig", 12);
+ try expect.at(&lines, 9, "src/a.zig", 13);
// `--- x` is a removed line, gone from past the hunk's new lines.
- try expect.at(&lines, 10, "b/src/a.zig", 13);
- try expect.at(&lines, 11, "b/src/a.zig", 41);
- try expect.at(&lines, 12, "b/src/a.zig", 41);
- try expect.at(&lines, 13, "b/src/a.zig", 42);
- try expect.at(&lines, 14, "b/src/a.zig", 42);
+ try expect.at(&lines, 10, "src/a.zig", 13);
+ try expect.at(&lines, 11, "src/a.zig", 41);
+ try expect.at(&lines, 12, "src/a.zig", 41);
+ try expect.at(&lines, 13, "src/a.zig", 42);
+ try expect.at(&lines, 14, "src/a.zig", 42);
// A removed line of a deleted file: its old line in the old file.
const gone = targetAt(&lines, 21, false).?;
- try testing.expectEqualStrings("a/old.py", gone.path);
+ try testing.expectEqualStrings("old.py", gone.path);
try testing.expectEqual(@as(usize, 2), gone.line);
try testing.expect(gone.old_side);
try testing.expect(targetAt(&lines, 1, false) == null);
@@ -490,8 +535,6 @@ test "diff plain diff -u output: no a/ b/, timestamps after the names" {
try testing.expectEqual(@as(usize, 4), t.line);
// `--- old/x.zig` with `+++ new/x.zig` under it: the new file.
try testing.expectEqualStrings("new/x.zig", targetAt(&lines, 0, false).?.path);
- try testing.expectEqualStrings("new/x.zig", stripSide("new/x.zig"));
- try testing.expectEqualStrings("x.zig", stripSide("b/x.zig"));
}
test "diff -u in a command pane, its tabs turned to blanks by the terminal, names its files without the stamps" {
@@ -523,9 +566,9 @@ test "diff a --- line opens the new name when the +++ under it names another" {
\\+b
\\
);
- try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 0, false).?.path);
- try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 4, false).?.path);
- try testing.expectEqualStrings("b/new name.zig", targetAt(&lines, 5, false).?.path);
+ try testing.expectEqualStrings("new name.zig", targetAt(&lines, 0, false).?.path);
+ try testing.expectEqualStrings("new name.zig", targetAt(&lines, 4, false).?.path);
+ try testing.expectEqualStrings("new name.zig", targetAt(&lines, 5, false).?.path);
try testing.expect(targetAt(&lines, 2, false) == null);
}
@@ -569,3 +612,54 @@ test "diff a terminal's wrapped row stays in its line" {
for (lines[0..5]) |line| last = w.step(line).kind;
try testing.expectEqual(Kind.added, last);
}
+
+test "diff git's side prefixes: dropped when both sides carry different ones, else kept" {
+ const cases = [_]struct { header: []const u8, new: []const u8, strip: bool }{
+ .{ .header = "diff --git a/src/x.zig b/src/x.zig", .new = "src/x.zig", .strip = true },
+ // diff.mnemonicPrefix: commit, index, work tree.
+ .{ .header = "diff --git c/src/x.zig w/src/x.zig", .new = "src/x.zig", .strip = true },
+ .{ .header = "diff --git i/src/x.zig w/src/x.zig", .new = "src/x.zig", .strip = true },
+ .{ .header = "diff --git a/old name.zig b/new name.zig", .new = "new name.zig", .strip = true },
+ // --no-prefix: no prefix at all, and a real top-level `a/` kept.
+ .{ .header = "diff --git src/x.zig src/x.zig", .new = "src/x.zig", .strip = false },
+ .{ .header = "diff --git a/x.zig a/x.zig", .new = "a/x.zig", .strip = false },
+ .{ .header = "diff --git a/b x a/b x", .new = "a/b x", .strip = false },
+ };
+ for (cases) |c| {
+ const h = gitHeader(c.header).?;
+ try testing.expectEqualStrings(c.new, h.new);
+ try testing.expectEqual(c.strip, h.strip);
+ }
+ try testing.expect(gitHeader("diff -u old new") == null);
+}
+
+fn pathsOf(comptime text: []const u8) ![3][]const u8 {
+ const lines = splitLines(text);
+ var w: Walk = .{};
+ for (lines) |line| _ = w.step(line);
+ const t = targetAt(&lines, 3, false) orelse return error.NoTarget;
+ return .{ w.old_path, w.new_path, t.path };
+}
+
+test "diff a section's paths are the repository's names under every prefix git writes, and as written elsewhere" {
+ const expect = struct {
+ fn paths(got: [3][]const u8, old: []const u8, new: []const u8) !void {
+ try testing.expectEqualStrings(old, got[0]);
+ try testing.expectEqualStrings(new, got[1]);
+ try testing.expectEqualStrings(new, got[2]);
+ }
+ };
+ try expect.paths(try pathsOf("diff --git a/src/x.zig b/src/x.zig\n--- a/src/x.zig\n+++ b/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig");
+ try expect.paths(try pathsOf("diff --git c/src/x.zig w/src/x.zig\n--- c/src/x.zig\n+++ w/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig");
+ try expect.paths(try pathsOf("diff --git i/src/x.zig w/src/x.zig\n--- i/src/x.zig\n+++ w/src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig");
+ // --no-prefix, in a repository with a real directory `a`.
+ try expect.paths(try pathsOf("diff --git a/x.zig a/x.zig\n--- a/x.zig\n+++ a/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "a/x.zig", "a/x.zig");
+ try expect.paths(try pathsOf("diff --git src/x.zig src/x.zig\n--- src/x.zig\n+++ src/x.zig\n@@ -1 +1 @@\n-a\n+b\n"), "src/x.zig", "src/x.zig");
+ // Plain diff -u never strips, even a name that looks prefixed.
+ try expect.paths(try pathsOf("--- old/x.zig\t2026-09-30 10:00:00 +0000\n+++ new/x.zig\t2026-09-30 11:00:00 +0000\n@@ -1 +1 @@\n-a\n"), "old/x.zig", "new/x.zig");
+ try expect.paths(try pathsOf("--- a/x.zig\n+++ b/x.zig\n@@ -1 +1 @@\n-a\n"), "a/x.zig", "b/x.zig");
+ // /dev/null stays itself.
+ const made = try pathsOf("diff --git a/n.zig b/n.zig\n--- /dev/null\n+++ b/n.zig\n@@ -0,0 +1 @@\n+a\n");
+ try testing.expectEqualStrings("/dev/null", made[0]);
+ try testing.expectEqualStrings("n.zig", made[1]);
+}
diff --git a/src/look.zig b/src/look.zig
index e22f5705..afb5b760 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1527,9 +1527,11 @@ const DiffLook = union(enum) { none, done, at: []const u8 };
/// 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 (walking up from the diff's
-/// directory to a `.git` or `.jj`), `a/` and `b/` dropped, else the
-/// directory's own. A file that is not there opens nothing and says so.
+/// 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
@@ -1573,8 +1575,7 @@ fn diffLook(p: *Pardes, id: usize, pane: *Pane, operand: []const u8, clicked: bo
}
/// The file a diff names, from `base` (the diff's directory): tried in the
-/// repository `base` is in, then in `base`, as git writes it (`b/x`, the
-/// repository's `x`) and as written.
+/// 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;
@@ -1593,11 +1594,9 @@ fn diffFile(base: []const u8, path: []const u8, out: *[4096]u8) ?[]const u8 {
} else null;
for ([_]?[]const u8{ root, base }) |maybe| {
const at = maybe orelse continue;
- for ([_][]const u8{ diff.stripSide(path), path }) |name| {
- var joined: [4096]u8 = undefined;
- const full = std.fmt.bufPrint(&joined, "{s}/{s}", .{ at, name }) catch continue;
- if (fs.resolveOs(full, out)) |found| if (!found.dir) return found.path;
- }
+ 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;
}
return null;
}
@@ -2175,10 +2174,10 @@ test "Look on a diff line of a file that is not here says so and opens nothing"
var after: usize = 0;
for (p.panes) |slot| after += @intFromBool(slot != null);
try std.testing.expectEqual(before, after);
- try std.testing.expectEqualStrings("Look: b/src/elsewhere.zig: no such file here", pane.msg[0..pane.msg_len]);
+ 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: a/old.zig: deleted by this diff", pane.msg[0..pane.msg_len]);
+ 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" {
@@ -2303,3 +2302,56 @@ test "Look outside a diff pane expands and looks as ever" {
const operand = exec.pointerOperand(p, pane, pointed.sel);
try std.testing.expect(operand.text == null or !std.mem.eql(u8, operand.text.?, "+const a = 1;"));
}
+
+test "Look on a diff under each git prefix opens the repository's file, and a real a/ directory under --no-prefix" {
+ if (!platform_has_fs) return;
+ const Case = struct { text: []const u8, file: []const u8 };
+ const cases = [_]Case{
+ .{ .text = "diff --git a/src/x.zig b/src/x.zig\n--- a/src/x.zig\n+++ b/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" },
+ .{ .text = "diff --git c/src/x.zig w/src/x.zig\n--- c/src/x.zig\n+++ w/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" },
+ .{ .text = "diff --git i/src/x.zig w/src/x.zig\n--- i/src/x.zig\n+++ w/src/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/src/x.zig" },
+ // --no-prefix: `a/x.zig` is the file x.zig in the directory a, and
+ // a `x.zig` at the top is never taken for it.
+ .{ .text = "diff --git a/x.zig a/x.zig\n--- a/x.zig\n+++ a/x.zig\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n", .file = "/a/x.zig" },
+ };
+ for (cases) |c| {
+ var fx: DiffFixture = undefined;
+ try fx.init(true, &.{ .{ "src/x.zig", "uno\ntwo\n" }, .{ "a/x.zig", "uno\ntwo\n" }, .{ "x.zig", "top\n" } }, c.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, c.text);
+ DiffFixture.click(p, pane, 6); // `two`, a context line: new line 2
+ try std.testing.expect(!p.look_missed);
+ try expectAt(p, c.file, 2);
+ DiffFixture.click(p, pane, 1); // `---`
+ try std.testing.expect(!p.look_missed);
+ try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, c.file));
+ }
+}
+
+test "Look on a diff says a missing or deleted file by its repository name, a plain diff's as written" {
+ 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]);
+}