summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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')