summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 10:18:59 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commitd9c6786e19567878e3c89ae212296ce941484096 (patch)
tree23b973692fc258bd7910cadf135d06776015da6f
parent5b27be228f5961dee8727bec7480f375dfb588ed (diff)
downloadpardes-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.md3
-rw-r--r--docs/fs.md4
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/ninep/pane.zig32
-rw-r--r--test/fs.py8
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')` |
diff --git a/docs/fs.md b/docs/fs.md
index 1b85e4c3..6060949b 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -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");
diff --git a/test/fs.py b/test/fs.py
index c7552a2b..ca1ca70e 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -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')