diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 21:10:00 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 591d3c7baecc434527d02e5894ac0240d2fa1c8d (patch) | |
| tree | c09c908d58cfc49f796759e6b5f07e86cc7d2267 | |
| parent | 6a815117d242688c947a1788e4ffda9ff2bce959 (diff) | |
| download | pardes-591d3c7baecc434527d02e5894ac0240d2fa1c8d.tar.gz pardes-591d3c7baecc434527d02e5894ac0240d2fa1c8d.zip | |
:N on a PDF turns to page N, from the pane and from the root's look; @pN:N answers ok with no err; an address in a pane with no text names the pane
A PDF pane took :N as a word to search for, and @p<pdf>:N failed as
"is no text to address" after the page had nothing to do with it, an
err in the log for a look that meant something. A PDF's address is now
its page: :N turns to it, one past the last is the has-no-page miss,
and a byte offset or a pattern is refused saying a PDF's address is a
page. The refusal for any other textless pane named an empty path when
the look was a bare :addr; it names the image or @p<serial> now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/look.zig | 23 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 10 | ||||
| -rw-r--r-- | test/pdf.zig | 45 |
3 files changed, 77 insertions, 1 deletions
diff --git a/src/look.zig b/src/look.zig index 3cd1faa5..4d23582b 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1413,9 +1413,13 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { } const pane = p.panes[target] orelse return; const f = pane.file orelse { + if (comptime pdf_enabled) if (pane.pdf) |*pv| return lookPdfPage(p, id, target, pv, expr); if (by_serial and pane.isTerminal()) return lookTerminalAddress(p, id, target, expr); + // Named as the pane is, the bare `:addr` (path empty) included. + var serial_name: [32]u8 = undefined; + const subject = if (pane.image) |iv| iv.path else if (path.len > 0) path else std.fmt.bufPrint(&serial_name, config.pane_addr ++ "{d}", .{pane.serial}) catch "the pane"; var cut: [miss_path_cap + "…".len]u8 = undefined; - return missed(p, id, "{s} is no text to address", .{missText(path, &cut)}); + return missed(p, id, "{s} is no text to address", .{missText(subject, &cut)}); }; if (!by_serial and path.len > 0 and (f.output != null or !std.mem.endsWith(u8, f.path, std.fs.path.basename(path)))) return; var a: Addr = .{ .text = f.content, .lim = null, .expr = expr }; @@ -1433,6 +1437,20 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { p.active = target; } +/// A PDF's address is its page: `:N` turns to page N, one past its last a +/// miss. It has no text to hold a byte offset (`#N`) or a pattern: any +/// other address is refused with what it takes. +fn lookPdfPage(p: *Pardes, id: usize, target: usize, pv: anytype, expr: []const u8) void { + var cut: [miss_path_cap + "…".len]u8 = undefined; + const page = std.fmt.parseInt(usize, std.mem.trim(u8, expr, " \t"), 10) catch 0; + if (page == 0) return missed(p, id, "{s}: a PDF has no text to address; its address is a page, :N", .{missText(pv.path, &cut)}); + if (page > pv.page_count) return missedPage(p, id, pv.path, page); + const pane = p.panes[target].?; + clearNavigationSelection(p, pane); + if (pv.focusLocation(p.pdf_gpa, page, 0)) panes.Pdf.resetPageChrome(pane); + p.active = target; +} + /// `@p<terminal>:<address>`: any address over the terminal's body as its /// logical lines read (the lines a +Search lists as `@p<serial>:<line>`), /// from its cursor; the match is selected there, as a +Search row's is. @@ -1599,6 +1617,9 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void { // 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). 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] == ':') + return lookAddress(p, id, "", trimmed[1..]); 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. diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 4aa38292..a14b1efb 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1671,6 +1671,16 @@ test "Save path of a file pane logs a save record naming that path" { try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "save {d} /tmp/pardes-save-copy.txt\n", .{serial}))); } +test "an address looked at in an image pane names the image, never an empty subject" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const id = p.freeSlot().?; + const doc = try p.newDocPane(id); + doc.image = .{ .path = try p.gpa.dupe(u8, "/tmp/pardes-pic.ppm") }; + _ = wr(p, Node.of(doc.serial, .look), ":/x/\n"); + try testing.expect(th.logHas(p, "look: /tmp/pardes-pic.ppm is no text to address")); +} + test "Save and Save path on an image pane are refused with why, never a silent success" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/test/pdf.zig b/test/pdf.zig index fbd580b3..fe084cc1 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -407,6 +407,51 @@ test "Recent keeps a PDF as it keeps a file: open, then closed at its page" { try std.testing.expect(std.mem.indexOf(u8, out.written(), try std.fmt.bufPrint(&want, "{s}:1:1 open\n", .{path})) != null); } +test "`:N` on a PDF turns to page N, from the pane and from the root's look, and `@pN:N` logs no err" { + if (!pdf_enabled or platform == .web) return; + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const fixture = try pdf_impl.makeOutlineTestPdf(gpa); + defer gpa.free(fixture); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "outline.pdf", .data = fixture }); + var path_buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/outline.pdf", .{tmp.sub_path}); + const p = try Pardes.init(gpa, .{ .file = path, .cols = 80, .rows = 28 }); + defer p.deinit(); + const doc = p.panes[0].?; + const pages = doc.pdf.?.page_count; + try std.testing.expect(pages >= 2); + const failures = p.fs.failures; + pardes.look.lookAt(p, 0, ":2"); + try std.testing.expect(!p.look_missed); + try std.testing.expectEqual(@as(usize, 1), doc.pdf.?.page); + var line: [64]u8 = undefined; + pardes.look.lookAt(p, 0, try std.fmt.bufPrint(&line, "@p{d}:1", .{doc.serial})); + try std.testing.expect(!p.look_missed); + try std.testing.expectEqual(@as(usize, 0), doc.pdf.?.page); + try std.testing.expectEqual(failures, p.fs.failures); + // Over 9P too: the root's look at `@pN:2` answers ok and logs no err. + const ctlfs = pardes.ctlfs; + const written = p.serveFs(.{ .tag = 1, .op = .write, .node = @intFromEnum(ctlfs.TopFile.look), .data = try std.fmt.bufPrint(&line, "@p{d}:2\n", .{doc.serial}) }); + try std.testing.expectEqual(ctlfs.Status.ok, written.status); + try std.testing.expectEqual(@as(usize, 1), doc.pdf.?.page); + const log = @intFromEnum(ctlfs.TopFile.log); + const h = p.serveFs(.{ .tag = 2, .op = .open, .node = log }).handle; + const record = p.fsPayload(p.serveFs(.{ .tag = 3, .op = .read, .node = log, .handle = h, .size = 1 << 16 })); + try std.testing.expect(std.mem.indexOf(u8, record, "\nerr ") == null); + _ = p.serveFs(.{ .tag = 4, .op = .release, .node = log, .handle = h }); + while (p.nextEffect()) |_| {} + // A byte offset or a pattern is no PDF address: refused with words. + pardes.look.lookAt(p, 0, ":#3"); + try std.testing.expect(p.look_missed); + try std.testing.expect(std.mem.indexOf(u8, doc.msg[0..doc.msg_len], "its address is a page, :N") != null); + p.look_missed = false; + pardes.look.lookAt(p, 0, try std.fmt.bufPrint(&line, ":{d}", .{pages + 1})); + try std.testing.expect(p.look_missed); + try std.testing.expectEqual(@as(usize, 1), doc.pdf.?.page); // where the 9P look left it +} + test "a look at a PDF's page past its last is a miss, never the last page" { if (!pdf_enabled or platform == .web) return; const gpa = std.testing.allocator; |
