diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 10:18:59 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | d9c6786e19567878e3c89ae212296ce941484096 (patch) | |
| tree | 23b973692fc258bd7910cadf135d06776015da6f | |
| parent | 5b27be228f5961dee8727bec7480f375dfb588ed (diff) | |
| download | pardes-d9c6786e19567878e3c89ae212296ce941484096.tar.gz pardes-d9c6786e19567878e3c89ae212296ce941484096.zip | |
Truncating data or xdata deletes only the addressed range, not the whole body
A shell's echo NEW > data opens data with OTRUNC, and truncate() spliced
the whole body away before the write replaced the addressed range: a
five-byte replacement erased a buffer. data and xdata hold what addr
names, so truncating one now deletes that range and nothing else; > on
data replaces the range and : > data deletes it, and only truncating
body clears the buffer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 3 | ||||
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | src/fs-help.txt | 2 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 32 | ||||
| -rw-r--r-- | test/fs.py | 8 |
5 files changed, 44 insertions, 5 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 9fbd93f6..7e61b73d 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -116,7 +116,8 @@ For a file or scratch pane, with `pane=$m/pane/<serial>`: | Append text | `echo text >> $pane/body` | `client.write(pane + '/body', b'text\n')` | | Replace all text | `echo text > $pane/body` | `client.write(pane + '/body', b'text\n', truncate=True)` | | Address a byte range | `echo '#0,#2' > $pane/addr` | `client.write(pane + '/addr', b'#0,#2')` | -| Replace that range | `printf 'pub fn' >> $pane/data` | `client.write(pane + '/data', b'pub fn')` | +| Replace that range | `printf 'pub fn' > $pane/data` (`>>` too) | `client.write(pane + '/data', b'pub fn')` | +| Delete that range | `: > $pane/data` | truncate `data` (open with OTRUNC) | | Read the selection | `cat $pane/dot` (offsets), `cat $pane/sel` (text) | the same two reads | | Select the addressed range | `cp $pane/addr $pane/dot` | `client.write(pane + '/dot', client.read(pane + '/addr'))` | | Reload from disk | `echo get > $pane/ctl` | `client.write(pane + '/ctl', b'get\n')` | @@ -235,6 +235,10 @@ address expression (`#0,#5`, `/pattern/`, `2+1`); `addr` selects what `data` and `xdata` read or replace, `dot` is the editor's own selection and moving it scrolls the pane into view, and `limit` bounds a search and reads empty until it is set. Truncating a range file empties it; truncating `limit` lifts it. +Truncating `data` or `xdata` deletes the range `addr` names and nothing +else, so a shell's `echo NEW > data` replaces that range, `: > data` +deletes it, and `>>` inserts at it; only truncating `body` empties the +whole buffer. `addr` belongs to the pane rather than to a client and keeps what was written until someone writes or truncates it, so writing an address and reading it back evaluates it, which is what acme(4) promises of its own `addr`. diff --git a/src/fs-help.txt b/src/fs-help.txt index b6812318..3f7357d4 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -38,7 +38,7 @@ Pitfalls, one each: rmdir closes a pane even when it is dirty; index and pane/<n>/dirty show the flag. addr, dot and limit read the same pair of offsets they take, so cp between them works. dirty, mark and scroll read "0" or "1" and take "0" or "1"; truncating limit lifts it. - Writing body appends; open it with truncation to replace; writes are whole UTF-8 sequences. + body: >> appends, > replaces it all. data: > replaces (and : > deletes) just the addr range. tag reads path++text; writes append to the text (newlines too); truncating clears it, and dot or addr. A terminal's body is a history snapshot frozen per open; pty/data is the live stream. A failing command is reported in the editor and in log, not as a write error; a bad line fails the write. diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 1e549107..822e0704 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -568,16 +568,27 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize { } /// Truncation empties whatever the file holds: the body, the tag's editable -/// tail (acme's `cleartag`), a range. Every other setattr just answers fresh -/// attributes. +/// tail (acme's `cleartag`), a range. `data` and `xdata` hold what `addr` +/// addresses, so truncating one deletes that range and no more, and a +/// shell's `echo new > data` replaces it. Every other setattr just answers +/// fresh attributes. pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { const pf = &pane.fs; switch (file) { - .body, .data, .xdata => if (fileOf(pane) != null) { + .body => if (fileOf(pane) != null) { _ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err; pf.addr = .{}; setDot(pane, .{}); }, + .data, .xdata => if (fileOf(pane) != null) { + clampAddr(pf, bodyOf(pane).len); + const q0 = pf.addr.q0; + const q1 = @max(q0, pf.addr.q1); + const before = dotOf(pane); + _ = spliceBody(p, pane, q0, q1, "") orelse return .err; + setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) }); + pf.addr = .{ .q0 = q0, .q1 = q0 }; + }, .tag => { const empty = p.gpa.alloc(u8, 0) catch return .err; // An edit like a keyboard one: `u` in the tag brings it back. @@ -1036,6 +1047,21 @@ test "errors append to one +Errors buffer per directory" { try testing.expectEqual(@as(usize, 1), found); } +test "truncating data deletes only the addressed range, so > data replaces it" { + const p = try withFile(testing.allocator, "hello world\nsecond line\n"); + defer p.deinit(); + const serial = serialOf(p); + for ([_]PaneFile{ .data, .xdata }) |file| { + _ = call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .body), .truncate = true }); + _ = wr(p, Node.of(serial, .body), "hello world\nsecond line\n"); + _ = wr(p, Node.of(serial, .addr), "#0,#5"); + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .setattr, .node = Node.of(serial, file), .truncate = true }).reply.status); + try testing.expectEqualStrings(" world\nsecond line\n", p.panes[0].?.file.?.content); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, file), "HOWDY").reply.status); + try testing.expectEqualStrings("HOWDY world\nsecond line\n", p.panes[0].?.file.?.content); + } +} + test "setattr truncation empties the body and answers fresh attributes" { const gpa = testing.allocator; const p = try withFile(gpa, "one\ntwo\n"); @@ -338,6 +338,14 @@ def discovery(binary, embedded=False): assert client.read('/src/pardes.zig').startswith(b'const std') assert client.stat('/src/pardes.zig')['mode'] == 0o444 assert b'pub const Pardes' in client.read(f'/pane/{look(client, "/virtual/src/pardes.zig")}/body') + # An open of data with OTRUNC (a shell's >) deletes the addressed + # range only, so the write after it replaces that range. + scratch = new_pane(client, b'hello world\nsecond line\n') + client.write(f'/pane/{scratch}/addr', b'#0,#5') + client.write(f'/pane/{scratch}/data', b'HOWDY', truncate=True) + assert client.read(f'/pane/{scratch}/body') == b'HOWDY world\nsecond line\n' + client.remove(f'/pane/{scratch}') + print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus, the ctl split and commands behave') |
