From 69b6d714b7fc7788a1bf1c2305262a6e861253e8 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 02:12:34 -0300 Subject: 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 --- src/ninep/addr.zig | 81 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 20 deletions(-) (limited to 'src') 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); +} -- cgit v1.3