summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig134
1 files changed, 128 insertions, 6 deletions
diff --git a/src/look.zig b/src/look.zig
index 4d23582b..922504c0 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1238,6 +1238,15 @@ test "a miss names a long path by its start and its end, its middle given up to
try std.testing.expectEqualStrings("short/path.txt", missText("short/path.txt", &cut));
}
+/// A file that is there but would not open: said with its path, `look:
+/// <path>: permission denied`, the path cut in its middle when long.
+fn lookOpenFailed(p: *Pardes, id: usize, path: []const u8, err: anyerror) void {
+ if (@import("Messages.zig").dialReason(err) != null) return p.reportError(id, path, err);
+ var cut: [miss_path_cap + "…".len]u8 = undefined;
+ var said: [miss_path_cap + 16]u8 = undefined;
+ p.reportError(id, std.fmt.bufPrint(&said, "look: {s}", .{missText(path, &cut)}) catch "look", err);
+}
+
fn missedPage(p: *Pardes, id: usize, path: []const u8, page: usize) void {
var cut: [miss_path_cap + "…".len]u8 = undefined;
missed(p, id, "{s} has no page {d}", .{ missText(path, &cut), page });
@@ -1262,6 +1271,24 @@ fn zeroColumn(rest: []const u8) bool {
return false;
}
+/// Whether `text` ends in a `:line:col` place (each spelling zeroColumn
+/// knows) whose column is 0, line 0 included.
+fn zeroColumnTail(text: []const u8) bool {
+ var i: usize = 0;
+ while (std.mem.indexOfScalarPos(u8, text, i, ':')) |at| : (i = at + 1) {
+ const rest = text[at..];
+ for (rest[1..]) |c| if (std.mem.indexOfScalar(u8, "0123456789:-", c) == null) break else {
+ if (rest.len > 1 and std.ascii.isDigit(rest[1]) and zeroColumn(rest)) return true;
+ };
+ }
+ return false;
+}
+
+test "a zero column ends a look's text, line 0 included" {
+ for ([_][]const u8{ "f.txt:0:0", "f.txt:3:0", "dir/f.txt:3:2-0" }) |t| try std.testing.expect(zeroColumnTail(t));
+ for ([_][]const u8{ "f.txt:3", "f.txt:3:1", "f.txt:0", "f.txt", "a:b:0x" }) |t| try std.testing.expect(!zeroColumnTail(t));
+}
+
test "a zero column after a path is found, in every spelling a look takes" {
for ([_][]const u8{ ":3:0", ":3:0-4", ":3:2-0", ":3:2-5:0" }) |rest| try std.testing.expect(zeroColumn(rest));
for ([_][]const u8{ "", ":3", ":3:1", ":3:2-5", ":3:2-5:7", ":30:10" }) |rest| try std.testing.expect(!zeroColumn(rest));
@@ -1567,16 +1594,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;
}
@@ -1616,6 +1672,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
const trimmed = std.mem.trim(u8, txt, " \t\r\n");
// acme's `file:addr` and `:addr` (look.c:450-543): an address after
// the colon, evaluated in that file from its dot (openfile, look.c:675).
+ // `file:3:0`, and `file:0:0` too: columns count from 1, as addr's do,
+ // and a zero is refused in addr's words rather than taken as no column
+ // (or, after line 0, as an address).
+ if (zeroColumnTail(trimmed)) return p.reportFailure(id, @import("ninep/addr.zig").e_col_zero);
if (addressed(trimmed)) |ad| return lookAddress(p, id, ad.path, ad.expr);
// `:N` on a PDF is its page N, never a search for the text.
if (comptime pdf_enabled) if (pane.hasPdf() and trimmed.len > 1 and trimmed[0] == ':')
@@ -1623,7 +1683,6 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
const pl = parsePathLine(trimmed);
// `file:3:0`: columns count from 1, as addr's do, and a zero is refused
// in addr's words rather than taken as no column.
- if (pl.at.line != 0 and zeroColumn(trimmed[pl.path.len..])) return p.reportFailure(id, @import("ninep/addr.zig").e_col_zero);
if (comptime pdf_enabled) if (pane.hasPdf()) clearNavigationSelection(p, pane);
if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return;
var realbuf: [4096]u8 = undefined;
@@ -1711,7 +1770,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
if (!focusPaneByPath(p, target.path, target.at)) {
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
- return p.reportError(id, if (@import("Messages.zig").dialReason(err) != null) target.path else "look", err);
+ return lookOpenFailed(p, id, target.path, err);
if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
const failures = p.fs.failures;
exec.placeNew(p, id, id, free, .doc);
@@ -1733,7 +1792,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
if (focusPaneByPath(p, target.path, .{})) return;
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const made = panes.Image.create(p, free, target.path, &.{}) catch |err|
- return p.reportError(id, if (@import("Messages.zig").dialReason(err) != null) target.path else "look", err);
+ return lookOpenFailed(p, id, target.path, err);
// One it cannot read (corrupt, truncated, not what its name says)
// is said, a failure, rather than shown as a blank pane.
if (!@import("image.zig").readable(made.image.?.raw)) {
@@ -2230,6 +2289,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();