diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 16:43:44 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 319325b74f3c97cad9988faebc7c6afda26cad08 (patch) | |
| tree | 8b12f6862c0906fbc39f1d5b9ac100688833978d /src | |
| parent | 79fb1b77b8c9993fb36c250daf36970807f767c9 (diff) | |
| download | pardes-319325b74f3c97cad9988faebc7c6afda26cad08.tar.gz pardes-319325b74f3c97cad9988faebc7c6afda26cad08.zip | |
A 9P write scrolls only the view: dot moves as acme's textinsert moves it, never to the end of the write
writeData, flushBatch and writeBody showed what they wrote with
showOffset, which put the cursor at the write's end. The cursor is dot's
head, so a data write or a body append moved dot there, and a `sel` write
after it replaced the wrong text. showOffset now scrolls the view to the
offset and leaves the cursor, pinned, where it was. Dot shifts only by
the rule it already followed, acme's: an insert before dot shifts it, one
at its start or inside it grows it, one at its end or after it leaves it.
The test covers each case, then a body append followed by a sel write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/ninep/pane.zig | 45 |
1 files changed, 43 insertions, 2 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 7526075b..fef0f85f 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -231,14 +231,22 @@ pub fn setDot(pane: *Pane, r: State.Range) void { pane.ensureCursorVisible(); } +/// Scrolls `off` into view and nothing else: the cursor is dot's head, and +/// a write shown is not a write that moves dot (acme's textinsert leaves +/// dot where its rule puts it; only the view follows the text). pub fn showOffset(pane: *Pane, off: usize) void { const text = bodyOf(pane); const c = positionAt(pane, text, @min(off, text.len)); + // Where dot is now, kept: pinned, so the view moving under it does not + // move it to the view's top (pinCursor). + pane.pinCursor(); + const row = pane.body.cur_row; + const col = pane.body.cur_col; pane.body.cur_row = @intCast(c.row); pane.body.cur_col = @intCast(c.col); - pane.body.cur_pinned = true; - pane.body.sticky_col = -1; pane.ensureCursorVisible(); + pane.body.cur_row = row; + pane.body.cur_col = col; } pub fn clampAddr(pf: *State, len: usize) void { @@ -1350,6 +1358,39 @@ test "after a failed address, one that goes from it says there is none" { try testing.expectEqual(Status.ok, wr(p, addr, ".").reply.status); } +test "a 9P write moves dot only as acme's textinsert does, and scrolls only the view" { + const Case = struct { at: u32, want: State.Range }; + // dot is `cd` of `abcdef\n` (2..4); two bytes go in at `at`. + for ([_]Case{ + .{ .at = 0, .want = .{ .q0 = 4, .q1 = 6 } }, // before: shifted + .{ .at = 2, .want = .{ .q0 = 2, .q1 = 6 } }, // at its start: grown + .{ .at = 3, .want = .{ .q0 = 2, .q1 = 6 } }, // inside: grown + .{ .at = 4, .want = .{ .q0 = 2, .q1 = 4 } }, // at its end: left + .{ .at = 6, .want = .{ .q0 = 2, .q1 = 4 } }, // after: left + }) |c| { + const p = try withFile(testing.allocator, "abcdef\n"); + defer p.deinit(); + const serial = serialOf(p); + const pane = p.panes[p.paneBySerial(serial).?].?; + setDot(pane, .{ .q0 = 2, .q1 = 4 }); + var addr: [16]u8 = undefined; + _ = wr(p, Node.of(serial, .addr), try std.fmt.bufPrint(&addr, "#{d}", .{c.at})); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .data), "XX").reply.status); + try testing.expectEqual(c.want, dotOf(pane)); + } + // A body append leaves dot, and a sel write after it replaces dot, not + // the text appended. + const p = try withFile(testing.allocator, "abcdef\n"); + defer p.deinit(); + const serial = serialOf(p); + const pane = p.panes[p.paneBySerial(serial).?].?; + setDot(pane, .{ .q0 = 2, .q1 = 4 }); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .body), "more\n").reply.status); + try testing.expectEqual(State.Range{ .q0 = 2, .q1 = 4 }, dotOf(pane)); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .sel), "YY").reply.status); + try testing.expectEqualStrings("abYYef\nmore\n", fileOf(pane).?.content); +} + test "a write of two lines to name is refused EINVAL, on a held open or not" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); |
