summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 02:12:34 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit69b6d714b7fc7788a1bf1c2305262a6e861253e8 (patch)
tree2ad39a1ec58a25c4b4e65b517ba29ea86dc5a3a5
parent3db73735e9a8d88878bc9ee343f226c033320913 (diff)
downloadpardes-69b6d714b7fc7788a1bf1c2305262a6e861253e8.tar.gz
pardes-69b6d714b7fc7788a1bf1c2305262a6e861253e8.zip
A line address past the last newline is sam's empty line there
Forward line addresses followed acme's number(), which refuses the empty line after a text's last newline, so 1 of an empty buffer and 2 of "a\n" were out of range and Edit 1i/header/ failed on an empty file. They now follow sam's lineaddr (editors/sam/address.c:180-215): #0,#0 and #2,#2. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md6
-rw-r--r--src/ninep/addr.zig81
2 files changed, 67 insertions, 20 deletions
diff --git a/docs/fs.md b/docs/fs.md
index d1be9f6a..8397ed88 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -483,6 +483,12 @@ pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``).
bash's builtin `printf` writes a line at a time, so write a block with a
heredoc or `env printf`.
+A line number counts newlines as sam's lineaddr does (editors/sam/
+address.c:180), so the empty line just past a text's last newline is an
+address: `1` of an empty text is `#0,#0`, `2` of `a\n` is `#2,#2`, and
+`Edit 1i/header/` on an empty file inserts; a line past that is `address
+out of range`.
+
`line:col` is a pardes extension to sam's addresses, the spelling Look
takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it
composes like any simple address (`12:5,14:1`, `12:5+#3`). The column is
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index 1e47183b..e1d6da0f 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -188,30 +188,43 @@ pub const Addr = struct {
while (q0 > 0 and a.text[q0 - 1] != '\n') q0 -= 1;
return .{ .q0 = clip(q0), .q1 = clip(q1) };
},
- '+' => {
- if (q1 > 0) while (q1 < a.text.len and a.text[q1 - 1] != '\n') {
- q1 += 1;
- };
- q0 = q1;
- },
- else => {
- q0 = 0;
- q1 = 0;
- },
+ else => {},
}
- while (line > 0 and q1 < a.text.len) {
- const ch = a.text[q1];
- q1 += 1;
- if (ch == '\n' or q1 == a.text.len) {
- line -= 1;
- if (line > 0) q0 = q1;
+ // Forward, sam's lineaddr (editors/sam/address.c:180-215), not acme's
+ // number(): line n is found by counting newlines, and a line just
+ // past the last newline is an empty one there, so `1` of an empty
+ // text is #0,#0 and `2` of "a\n" is #2,#2; only past that is it out
+ // of range.
+ const nc = a.text.len;
+ var p: usize = 0;
+ var p1: usize = 0;
+ if (n == 0) {
+ if (dir == 0 or q1 == 0) return .{ .q0 = 0, .q1 = 0 };
+ p1 = q1;
+ p = q1 - 1;
+ } else {
+ var k: u32 = 1;
+ if (dir != 0 and q1 > 0) {
+ p = q1 - 1;
+ k = @intFromBool(a.text[p] == '\n');
+ p += 1;
+ }
+ while (k < n) {
+ if (p >= nc) {
+ a.err = e_range;
+ return null;
+ }
+ if (a.text[p] == '\n') k += 1;
+ p += 1;
}
+ p1 = p;
}
- if (line > 0) {
- a.err = e_range;
- return null;
+ while (p < nc) {
+ const ch = a.text[p];
+ p += 1;
+ if (ch == '\n') break;
}
- return .{ .q0 = clip(q0), .q1 = clip(q1) };
+ return .{ .q0 = clip(p1), .q1 = clip(p) };
}
/// acme's regexp(): forward from the end of `r` to the limit, wrapping
@@ -476,3 +489,31 @@ test "the address language, form by form" {
_ = th.wr(p, addr, "#0");
try testing.expectEqual(E.INVAL, th.wr(p, addr, nested).errno());
}
+
+test "a line just past the last newline is an empty one there, as sam's lineaddr has it" {
+ const Case = struct { text: []const u8, expr: []const u8, q0: u32, q1: u32 };
+ for ([_]Case{
+ .{ .text = "", .expr = "1", .q0 = 0, .q1 = 0 },
+ .{ .text = "a\n", .expr = "2", .q0 = 2, .q1 = 2 },
+ .{ .text = "a\nb", .expr = "2", .q0 = 2, .q1 = 3 },
+ .{ .text = "a\nb\n", .expr = "1", .q0 = 0, .q1 = 2 },
+ .{ .text = "a\nb\n", .expr = "0", .q0 = 0, .q1 = 0 },
+ .{ .text = "a\nb\n", .expr = "1+1", .q0 = 2, .q1 = 4 },
+ .{ .text = "a\nb\n", .expr = "$", .q0 = 4, .q1 = 4 },
+ }) |c| {
+ var a: Addr = .{ .text = c.text, .lim = null, .expr = c.expr };
+ const r = a.address(.{ .q0 = 0, .q1 = 0 }).?;
+ try testing.expectEqual(c.q0, r.q0);
+ try testing.expectEqual(c.q1, r.q1);
+ }
+ for ([_][2][]const u8{ .{ "", "2" }, .{ "a\n", "3" } }) |c| {
+ var a: Addr = .{ .text = c[0], .lim = null, .expr = c[1] };
+ try testing.expectEqual(null, a.address(.{ .q0 = 0, .q1 = 0 }));
+ try testing.expectEqualStrings(e_range, a.err);
+ }
+ // Edit's addresses are these: 1i on an empty file inserts.
+ const p = try th.withFile(testing.allocator, "");
+ defer p.deinit();
+ try testing.expectEqual(Status.ok, th.wr(p, Node.of(th.serialOf(p), .ctl), "Edit 1i/header/\n").reply.status);
+ try testing.expectEqualStrings("header", p.panes[0].?.file.?.content);
+}