diff options
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 276 |
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(); |
