summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:02:59 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitb43866d6ab784e518df8dbef374147367358ada9 (patch)
treeb49ffadf36cac764cb3fb6f4d343fa25d09dab73
parent9ca008a27c4ab83a29c1798f4eb081eef372624c (diff)
downloadpardes-b43866d6ab784e518df8dbef374147367358ada9.tar.gz
pardes-b43866d6ab784e518df8dbef374147367358ada9.zip
Save makes the directories its file goes in, always: for the pane's own name and Save path alike
Whether a Save made missing directories depended on how the name had come about. Config's pane did, a name written into a missing directory did (since kkrqxtsp), and `Save path` and every other name did not, failing ENOENT instead. There is now one rule: a Save makes the parent directories of the file it writes, a pane's own or a terminal's scrollback included, and the make_dirs flag is gone. A directory it cannot make still fails the write with why, as fs.py's /nonexistent-pardes-dir case shows. fs.py now also saves into missing directories by Save path and by a written name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md6
-rw-r--r--src/File.zig4
-rw-r--r--src/builtins.zig3
-rw-r--r--src/ninep/pane.zig18
-rw-r--r--src/pardes.zig14
-rw-r--r--test/fs.py14
6 files changed, 28 insertions, 31 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 334b6783..49b10a7b 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -364,8 +364,7 @@ are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike.
**`name`** reads the file name (a terminal's directory); a write renames the
buffer (relative to the pane's directory) and marks nothing dirty; `Save`
-then writes under the new name, making its directories first when the name
-went into one not there. A name is one line; refused (EINVAL) are a
+then writes under the new name. A name is one line; refused (EINVAL) are a
second line, a blank at either end, control bytes and non-UTF-8 (`bad
character in file name: a blank at its start`, ...). Up to 255 bytes a
component.
@@ -391,7 +390,8 @@ redo available, then `current`/`notcurrent` and a REPL's id if bound. It
takes:
- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give
- its rows to the pane above or below), `Save [path]`, `Collapse` (fold),
+ its rows to the pane above or below), `Save [path]` (making the
+ directories the file goes in first, whichever name it writes), `Collapse` (fold),
`Undo`/`Redo` (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]`
(a new terminal pane in its directory), and the column words acting on
its column: `Delcol`, `Left`/`Right`/`Up`/`Down`.
diff --git a/src/File.zig b/src/File.zig
index 43b921e3..84cec879 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -61,10 +61,6 @@ pub const State = struct {
/// edit to another length cannot be back to it, and is not hashed.
saved_len: ?usize = null,
watch_after_save: bool = false,
- /// Save makes the file's directory, and any above it, first: `Config`'s
- /// pane for an init file that is not there yet, whose directory may not
- /// be either.
- make_dirs: bool = false,
/// The file changed on disk under unsaved edits, which were kept (see
/// `changed`): its hash, so the same change is said once, until a Save
/// or get settles it. A Save warns once before overwriting it, as acme's
diff --git a/src/builtins.zig b/src/builtins.zig
index 8fe49649..5a2037cf 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1304,9 +1304,8 @@ pub const Config = struct {
// Not there yet: a pane named for it, which Save makes.
const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Config", error.NoPaneSlots);
c.p.newScratchFor(c.id, .scratch);
- const pane = c.p.panes[free] orelse return;
+ if (c.p.panes[free] == null) return;
@import("ninep/pane.zig").nameBuffer(c.p, free, path, false) catch |err| return c.p.reportError(c.id, "Config", err);
- pane.file.?.make_dirs = true;
c.p.active = free;
}
};
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 6a0fbba5..de7f7cd4 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -867,11 +867,6 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
error.OutOfMemory => E.NOMEM,
else => E.INVAL,
});
- // A name into a directory not there yet asks for it, as Config's pane
- // does: its Save makes the directories (`pardes new/dir/f` forwarded).
- if (comptime pardes.hosted) if (fs.localPath(std.fs.path.dirname(full) orelse "/")) |local| {
- if (!exec.isDirectory(local)) fileOf(pane).?.make_dirs = true;
- };
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -1392,19 +1387,6 @@ test "a name with a component over 255 bytes is refused, and a long path's faile
try testing.expect(std.mem.endsWith(u8, late, "/f.txt: no such directory"));
}
-test "a name into a directory not there has Save make it; one into a directory there does not" {
- if (comptime !pardes.hosted) return error.SkipZigTest;
- const p = try withFile(testing.allocator, "x\n");
- defer p.deinit();
- const serial = serialOf(p);
- const f = fileOf(p.panes[p.paneBySerial(serial).?].?).?;
- try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/pardes-no-such-dir-mk/sub/f.txt\n").reply.status);
- try testing.expect(f.make_dirs);
- f.make_dirs = false;
- try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/f.txt\n").reply.status);
- try testing.expect(!f.make_dirs);
-}
-
test "a name cut across writes is one name, applied once at its newline or its close" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
diff --git a/src/pardes.zig b/src/pardes.zig
index d8124588..841faf7a 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5401,6 +5401,15 @@ pub const Pardes = struct {
p.noteLateFailure(id);
}
+ /// A Save makes the directories its file goes in, and any above them,
+ /// first: one rule, for the pane's own name and `Save path` alike. A
+ /// directory it cannot make leaves the write to fail and say why.
+ fn makeParents(path: []const u8) void {
+ if (comptime !hosted) return;
+ const local = filesystem.localPath(path) orelse return;
+ filesystem.makeDirs(std.fs.path.dirname(local) orelse "/");
+ }
+
pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void {
if (p.panes[id]) |pane| if (pane.file) |*f| {
// The `-%` spelling fs.zig already uses for "make this dirty".
@@ -5490,8 +5499,7 @@ pub const Pardes = struct {
const was = f.saved_revision;
const failures = p.fs.failures;
f.saved_revision = f.revision;
- if (f.make_dirs) if (filesystem.localPath(f.path)) |local|
- filesystem.makeDirs(std.fs.path.dirname(local) orelse "/");
+ makeParents(f.path);
p.hostWriteFile(sf.pane, f.path, f.content);
const saved_pane = p.panes[sf.pane] orelse return;
if (saved_pane.serial != serial) return;
@@ -5520,6 +5528,7 @@ pub const Pardes = struct {
if (pane.file) |*f| {
const was = f.saved_revision;
const failures = p.fs.failures;
+ makeParents(st.path.slice());
p.hostWriteFile(st.pane, st.path.slice(), f.content);
const after = p.panes[st.pane] orelse return;
if (after.serial != st.serial) return;
@@ -5535,6 +5544,7 @@ pub const Pardes = struct {
if (!pane.isTerminal()) return;
const text = panes.terminal.screenTextAlloc(pane, p.gpa) catch return;
defer p.gpa.free(text);
+ makeParents(st.path.slice());
p.hostWriteFile(st.pane, st.path.slice(), text);
},
.write_dump => {
diff --git a/test/fs.py b/test/fs.py
index a1e2ae79..b02b9e45 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -381,8 +381,18 @@ def discovery(binary, embedded=False):
# Through a mount both read as EINVAL; the log says which and why.
assert f'err {scratch} addr: no match for regexp\n'.encode() in client.read('/log')
client.remove(f'/pane/{scratch}')
- # A save the host cannot do fails the write that asked for it,
- # and the log's err record names the path and why.
+ # A Save makes the directories its file goes in, whatever names
+ # it: the pane's own name or Save's path.
+ made_dirs = new_pane(client, b'deep\n')
+ client.write(f'/pane/{made_dirs}/ctl', f'Save {root}/save-made/a/b/x.txt\n'.encode())
+ assert (root / 'save-made/a/b/x.txt').read_bytes() == b'deep\n'
+ client.write(f'/pane/{made_dirs}/name', f'{root}/save-named/c/y.txt\n'.encode())
+ client.write(f'/pane/{made_dirs}/ctl', b'Save\n')
+ assert (root / 'save-named/c/y.txt').read_bytes() == b'deep\n'
+ client.remove(f'/pane/{made_dirs}')
+ # A save the host cannot do (a directory it may not make) fails
+ # the write that asked for it, and the log's err record names
+ # the path and why.
unsaved = new_pane(client, b'unsaved\n')
try:
client.write(f'/pane/{unsaved}/ctl', b'Save /nonexistent-pardes-dir/x.txt\n')