summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig276
1 files changed, 273 insertions, 3 deletions
diff --git a/src/look.zig b/src/look.zig
index 115dabe1..41cfdc0c 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -792,10 +792,16 @@ fn lookStand(pane: *Pane) LookFrom {
fn lookWalkPanes(p: *Pardes, out: *[MAX_PANES]usize) []const usize {
var n: usize = 0;
+ // A command pane walked from (`make`'s errors): its own places first.
+ if (commandPaneActive(p)) {
+ out[0] = p.active;
+ n = 1;
+ }
var i = p.n_look_src;
while (i > 0) {
i -= 1;
const id = p.paneBySerial(p.look_src[i]) orelse continue;
+ if (n > 0 and out[0] == id) continue;
out[n] = id;
n += 1;
}
@@ -890,11 +896,18 @@ fn lookSpanIn(p: *Pardes, pane: *Pane, from: LookFrom, delta: i32, budget: *usiz
return null;
}
+/// A command pane has the keyboard: `n` there walks its own output.
+fn commandPaneActive(p: *Pardes) bool {
+ const pane = p.panes[p.active] orelse return false;
+ return pane.command != null and pane.file == null and !p.header_focus;
+}
+
pub fn lookWalk(p: *Pardes, delta: i32) void {
var buf: [MAX_PANES]usize = undefined;
const order = lookWalkPanes(p, &buf);
if (order.len == 0) return;
- const owner = if (p.look_walk_owner) |serial| p.paneBySerial(serial) else null;
+ // From a command pane the walk starts there, whoever walked last.
+ const owner = if (commandPaneActive(p)) p.active else if (p.look_walk_owner) |serial| p.paneBySerial(serial) else null;
const active_at = std.mem.indexOfScalar(usize, order, p.active) orelse 0;
const at = if (owner) |wanted|
std.mem.indexOfScalar(usize, order, wanted) orelse active_at
@@ -1141,6 +1154,13 @@ fn resolveLookTarget(p: *Pardes, id: usize, txt: []const u8, realbuf: *[4096]u8)
// `.`, `..`, `./x` are this pane's directory's, not another's.
if (found != .none or pl.path.len == 0 or pl.path[0] == '/' or filesystem.dotRelative(pl.path)) break;
}
+ // Found nowhere else, a relative name is tried in Incl's directories,
+ // as acme's includename tries its (look.c:500).
+ if (found == .none and pl.path.len > 0 and pl.path[0] != '/' and !filesystem.dotRelative(pl.path))
+ for (p.incl.items) |dir| {
+ found = resolve(p, txt, dir, realbuf);
+ if (found != .none) break;
+ };
return found;
}
@@ -1734,7 +1754,9 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
const txt = p.scratch.allocator().dupe(u8, looked) catch return;
// A trailing `:` (`a.txt:2:3:`, as a compiler's message ends) is no
// part of the place, as a click's expansion leaves it off.
- const trimmed = std.mem.trimEnd(u8, std.mem.trim(u8, txt, " \t\r\n"), ":");
+ const bare = std.mem.trimEnd(u8, std.mem.trim(u8, txt, " \t\r\n"), ":");
+ // `<stdio.h>` written whole: the name inside, as a click on it takes.
+ const trimmed = if (bare.len > 2 and bare[0] == '<' and bare[bare.len - 1] == '>') bare[1 .. bare.len - 1] else bare;
// 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,
@@ -1787,6 +1809,8 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
if (t.at.line > pv.page_count) return missedPage(p, id, pv.path, t.at.line);
if (!pv.hitExists(p.pdf_gpa, @max(1, t.at.line), t.at.col)) return missedHit(p, id, pv.path, @max(1, t.at.line), t.at.col);
};
+ // A directory's pane, looked at again: read again.
+ if (target.file) |f| if (f.listing != null) return panes.Dir.show(p, id, t.id);
const before = p.active;
clearNavigationSelection(p, target);
focusPaneLine(p, t.id, t.at, .center);
@@ -1800,6 +1824,9 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void {
var said: [4200]u8 = undefined;
return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such directory", .{dir[0..@min(dir.len, 4096)]}) catch "look: no such directory");
};
+ // acme's: a pane listing it (Dir.zig). `DirLook terminal`, what
+ // pardes did before: `ls` in a terminal there.
+ if (p.settings.dir_look == .pane) return panes.Dir.look(p, id, dir);
for (p.panes, 0..) |slot, i| {
if (slot) |tt| if (std.mem.eql(u8, tt.cwdSlice(), dir) and exec.takesCommandLine(p, i)) {
clearNavigationSelection(p, tt);
@@ -2414,7 +2441,7 @@ test "a click, a 9P whole-line look and an empty event write-back take one expan
// 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);
+ const written = pardes.ctlfs.events.expandAt(p, pane, body, line_start + at.col, true, true);
try std.testing.expectEqualStrings(at.want, written);
// A 9P look of the whole line.
if (at.whole_line) {
@@ -2426,6 +2453,249 @@ test "a click, a 9P whole-line look and an empty event write-back take one expan
}
}
+test "every entry point to Look takes one expansion: a diff line and a file:12:5: line land in one place by each" {
+ if (!platform_has_fs) return;
+ const numbered = "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nline 12 here\nl13\n";
+ const diff_text =
+ \\diff --git a/src/x.zig b/src/x.zig
+ \\--- a/src/x.zig
+ \\+++ b/src/x.zig
+ \\@@ -11,2 +11,3 @@
+ \\ l11
+ \\-old
+ \\+line 12 here
+ \\
+ ;
+ const log_text = "src/x.zig:12:5: error: bad\n";
+ var fx: DiffFixture = undefined;
+ try fx.init(true, &.{.{ "src/x.zig", numbered }}, diff_text);
+ defer fx.tmp.cleanup();
+ const th = @import("ninep/testing.zig");
+ // `column` -1: wherever a click puts it, every other way the same.
+ const Case = struct { text: []const u8, name: []const u8, row: u16, col: u16, sweep: [2]u16, line: i32, column: i32 };
+ const cases = [_]Case{
+ // `+line 12 here` pointed at on its `+`: the hunk's new line 12.
+ .{ .text = diff_text, .name = "change.diff", .row = 6, .col = 0, .sweep = .{ 0, 12 }, .line = 11, .column = -1 },
+ // A compiler's place, its trailing `:` dropped: line 12, column 5.
+ .{ .text = log_text, .name = "build.log", .row = 0, .col = 2, .sweep = .{ 0, 14 }, .line = 11, .column = 4 },
+ };
+ for (cases) |case| {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const pane = try fx.open(p, case.text);
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = try std.fs.path.join(p.gpa, &.{ fx.dir, case.name });
+ pane.tag_expanded = false;
+ var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer frame.deinit();
+ const Entry = enum { click, sweep, enter, bare_look, look_file, event_write_back };
+ var column = case.column;
+ for (std.enums.values(Entry)) |entry| {
+ // From the pane, its cursor on the place, nothing selected,
+ // the target (once open) moved off it.
+ for (p.panes[1..]) |slot| if (slot) |other| {
+ other.body.cur_row = 0;
+ other.body.cur_col = 0;
+ };
+ p.active = 0;
+ pane.body.mode = .normal;
+ pane.body.vsel.active = false;
+ pane.body.msel.active = false;
+ pane.body.cur_row = case.row;
+ pane.body.cur_col = case.col;
+ pane.body.cur_pinned = true;
+ pane.file.?.scroll = 0;
+ p.sync();
+ _ = frame.reset(.retain_capacity);
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x0 = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect) + case.row;
+ switch (entry) {
+ .click => {
+ p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x0 + case.col, .row = y } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x0 + case.col, .row = y } });
+ },
+ .sweep => {
+ p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x0 + case.sweep[0], .row = y } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .drag, .col = x0 + case.sweep[1], .row = y } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x0 + case.sweep[1], .row = y } });
+ },
+ .enter => p.update(.{ .key = .{ .cp = Key.enter } }),
+ .bare_look => _ = p.executeBuiltinLine(0, "Look"),
+ .look_file => {
+ const line = modal.lineSlice(case.text, case.row);
+ // A diff line written whole; a place, as a script writes it.
+ const written = if (std.mem.eql(u8, case.name, "change.diff")) line else line[case.sweep[0] .. case.sweep[1] + 1];
+ _ = th.wr(p, @intFromEnum(pardes.ctlfs.TopFile.look), written);
+ },
+ .event_write_back => {
+ const body = pane.file.?.content;
+ const text = pardes.ctlfs.events.expandAt(p, pane, body, modal.lineStartOffset(body, case.row) + case.col, true, true);
+ lookAt(p, 0, p.scratch.allocator().dupe(u8, text) catch unreachable);
+ },
+ }
+ const landed = p.panes[p.active].?;
+ const f = landed.file orelse return error.NothingOpened;
+ if (column < 0) column = landed.body.cur_col;
+ if (!std.mem.endsWith(u8, f.path, "/src/x.zig") or landed.body.cur_row != case.line or landed.body.cur_col != column) {
+ std.debug.print("{s} by {s}: at {s} {d}:{d}\n", .{ case.name, @tagName(entry), f.path, landed.body.cur_row, landed.body.cur_col });
+ return error.EntryPointsDiffer;
+ }
+ }
+ }
+}
+
+test "a tag word looked at by a click and by Enter is one record to an event reader; an exec on a diff line takes its word, never the address" {
+ if (!platform_has_fs) return;
+ const diff_text =
+ \\diff --git a/src/x.zig b/src/x.zig
+ \\--- a/src/x.zig
+ \\+++ b/src/x.zig
+ \\@@ -1,1 +1,1 @@
+ \\-old
+ \\+line one
+ \\
+ ;
+ var fx: DiffFixture = undefined;
+ try fx.init(true, &.{.{ "src/x.zig", "line one\n" }}, diff_text);
+ defer fx.tmp.cleanup();
+ const th = @import("ninep/testing.zig");
+ 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 = true;
+ var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer frame.deinit();
+ p.sync();
+ _ = try p.render(frame.allocator());
+
+ // An exec on the `+` of a hunk line takes the word there; a look, the
+ // address the line names.
+ pane.body.cur_row = 5;
+ pane.body.cur_col = 0;
+ try std.testing.expectEqualStrings("+line", exec.operandAt(p, pane, cursorWordSel(p, pane), false, false).text.?);
+ try std.testing.expectEqualStrings("src/x.zig:1", exec.operandAt(p, pane, cursorWordSel(p, pane), false, true).text.?);
+ // An exec written back to event (`MX0 0`) takes the word too, on a
+ // header, a hunk header and a marker column; a look, the address.
+ const body = pane.file.?.content;
+ const events = pardes.ctlfs.events;
+ try std.testing.expectEqualStrings("diff", events.expandAt(p, pane, body, 0, true, false));
+ try std.testing.expectEqualStrings("@@", events.expandAt(p, pane, body, modal.lineStartOffset(body, 3), true, false));
+ try std.testing.expectEqualStrings("+line", events.expandAt(p, pane, body, modal.lineStartOffset(body, 5), true, false));
+ try std.testing.expectEqualStrings("src/x.zig", events.expandAt(p, pane, body, 0, true, true));
+ try std.testing.expectEqualStrings("src/x.zig:1", events.expandAt(p, pane, body, modal.lineStartOffset(body, 5), true, true));
+
+ // An event reader on the pane.
+ const event = pardes.ctlfs.Node.of(pane.serial, .event);
+ const opened = th.call(p, .{ .tag = 1, .op = .open, .node = event });
+ defer _ = th.call(p, .{ .tag = 9, .op = .release, .node = event, .handle = opened.reply.handle });
+ const tag = try tagline.tagText(p, p.scratch.allocator(), pane);
+ const at: u16 = @intCast(std.mem.lastIndexOf(u8, tag, "Del").?);
+ // A B3 click on the tag's Del, where the tag as shown has it...
+ const shown = (try tagline.shownTag(p, p.scratch.allocator(), pane)).text;
+ const shown_at = std.mem.lastIndexOf(u8, shown, "Del").?;
+ const shown_row: u16 = @intCast(std.mem.count(u8, shown[0..shown_at], "\n"));
+ const line_start = if (std.mem.lastIndexOfScalar(u8, shown[0..shown_at], '\n')) |nl| nl + 1 else 0;
+ const x = p.rects[0].x + TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(shown[line_start..shown_at]))) + 1;
+ const y = p.rects[0].y + shown_row;
+ p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = x, .row = y } });
+ p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = x, .row = y } });
+ var clicked_buf: [128]u8 = undefined;
+ const clicked = th.rd(p, event, 0, 4096).bytes;
+ @memcpy(clicked_buf[0..clicked.len], clicked);
+ // ...and Enter with the tag's cursor on it.
+ tagline.enterTag(p, pane);
+ pane.tag.mode = .normal;
+ pane.tag.cur_row = 0;
+ pane.tag.cur_col = at + 1;
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ const keyed = th.rd(p, event, 0, 4096).bytes;
+ // The same record but for its origin, mouse or keyboard.
+ try std.testing.expect(clicked.len > 2);
+ try std.testing.expectEqualStrings(clicked_buf[1..clicked.len], keyed[1..]);
+ try std.testing.expect(p.panes[0] != null);
+}
+
+test "Incl's directories find a name found nowhere else, <name> too; bare Incl lists them, Incl - empties them, and a dump keeps them" {
+ if (!platform_has_fs) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.createDirPath(std.testing.io, "inc");
+ try tmp.dir.createDirPath(std.testing.io, "src");
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "inc/pardes_incl_probe.h", .data = "#define X 1\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const root = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 });
+ defer p.deinit();
+ // C's two by default.
+ try std.testing.expectEqualStrings("/usr/include", p.incl.items[0]);
+ const pane = try p.setTestFile("#include <pardes_incl_probe.h>\n");
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = try std.fs.path.join(p.gpa, &.{ root, "src", "main.c" });
+ var line: [4200]u8 = undefined;
+ try std.testing.expect(p.executeBuiltinLine(0, try std.fmt.bufPrint(&line, "Incl {s}/inc", .{root})));
+ for ([_][]const u8{ "pardes_incl_probe.h", "<pardes_incl_probe.h>" }) |word| {
+ p.active = 0;
+ p.look_missed = false;
+ lookAt(p, 0, word);
+ try std.testing.expect(!p.look_missed);
+ try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/inc/pardes_incl_probe.h"));
+ }
+ // Bare, it lists them.
+ try std.testing.expect(p.executeBuiltinLine(0, "Incl"));
+ try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "/inc") != null);
+ // A dump keeps them.
+ try pardes.dump.dumpState(p);
+ const restored = try pardes.dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon");
+ defer restored.deinit();
+ try std.testing.expectEqual(p.incl.items.len, restored.incl.items.len);
+ try std.testing.expect(std.mem.endsWith(u8, restored.incl.items[restored.incl.items.len - 1], "/inc"));
+ // Emptied, the name is found nowhere.
+ try std.testing.expect(p.executeBuiltinLine(0, "Incl -"));
+ try std.testing.expectEqual(@as(usize, 0), p.incl.items.len);
+}
+
+test "n in a command pane walks its own file:line places, whoever walked last" {
+ if (!platform_has_fs) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "x.c", .data = "a\nb\nc\n" });
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "y.c", .data = "a\nb\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const doc = try p.setTestFile("x\n");
+ p.gpa.free(doc.file.?.path);
+ doc.file.?.path = try std.fs.path.join(p.gpa, &.{ dir, "main.c" });
+ while (p.nextEffect()) |_| {}
+ const dst = exec.execute(p, 0, "make") orelse return error.NoCommandPane;
+ while (p.nextEffect()) |_| {}
+ p.update(.{ .output = .{ .pane = @intCast(dst), .bytes = "x.c:3:1: error: bad\r\ny.c:2:1: warning: odd\r\n" } });
+ p.update(.{ .exited = .{ .pane = @intCast(dst), .status = 2 } });
+ p.update(.{ .eof = .{ .pane = @intCast(dst) } });
+ const cmd = p.panes[dst].?;
+ // A walk last made elsewhere, and another pane with places in it.
+ try std.testing.expect(p.executeBuiltinLine(0, "New"));
+ const scratch = p.panes[p.active].?;
+ _ = pardes.ctlfs.pane.spliceBody(p, scratch, 0, 0, "x.c:1:1 elsewhere\n") orelse return error.NoScratch;
+ p.look_walk_owner = scratch.serial;
+ p.active = dst;
+ cmd.focus = .body;
+ cmd.body.mode = .normal;
+ for ([_]u21{ 'g', 'g' }) |cp| p.update(.{ .key = .{ .cp = cp } });
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(dst, p.active);
+ const first = cmd.look_at orelse return error.NoSpot;
+ const lines = try edit.paneCursorLines(p, &cmd.body);
+ try std.testing.expect(std.mem.startsWith(u8, lines[@intCast(first.row)][@intCast(first.col0)..], "x.c:3:1"));
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(dst, p.active);
+ const second = cmd.look_at orelse return error.NoSpot;
+ try std.testing.expect(std.mem.startsWith(u8, lines[@intCast(second.row)][@intCast(second.col0)..], "y.c:2:1"));
+}
+
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();