summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/9p_io.zig17
-rw-r--r--src/builtins.zig14
-rw-r--r--src/exec.zig2
-rw-r--r--src/ninep/ctl.zig30
-rw-r--r--test/fs.py29
5 files changed, 88 insertions, 4 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 671a1852..46bbb2ba 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -316,9 +316,20 @@ pub const Listener = struct {
// mount: 9ns answers nothing else on it while a clunk is out, and
// the editor's step may be out opening a file through that mount.
if (fills) return conn.reply(&reply, core.fsPayload(reply));
- // A refusal answers at once: its text may be in the core's one
- // buffer for it, which a request run while this one waited would
- // write over.
+ // A refusal that left saves going (Putall's one refused among the
+ // rest) is answered once they have landed, its words copied out of
+ // the core's one buffer for them, which a request run meanwhile
+ // could write over.
+ if (reply.status == .err and core.effects_len != 0 and !core.quit) {
+ var kept: [320]u8 = undefined;
+ const words = kept[0..@min(reply.ename.len, kept.len)];
+ @memcpy(words, reply.ename[0..words.len]);
+ var waited = reply;
+ waited.ename = words;
+ pardes.turn.awaitSettled(epoch);
+ return conn.reply(&waited, "");
+ }
+ // Any other refusal answers at once, for the same buffer's sake.
if (reply.status == .err) return conn.reply(&reply, "");
// A write that quits the editor (Kill) is answered now: the editor
// is on its way out and will settle nothing this could wait for,
diff --git a/src/builtins.zig b/src/builtins.zig
index 129dd659..70ca93a1 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1191,6 +1191,20 @@ pub const Incl = struct {
p.incl.clearRetainingCapacity();
return p.setMessage(c.id, "Incl: none");
}
+ // All checked before any is taken: `-` clears, alone; a directory
+ // is absolute (or ~) and there.
+ var check = std.mem.tokenizeAny(u8, typed, " \t");
+ while (check.next()) |dir_word| {
+ var said: [320]u8 = undefined;
+ const shown = dir_word[0..@min(dir_word.len, 200)];
+ if (std.mem.eql(u8, dir_word, "-")) return p.reportFailure(c.id, "Incl: invalid: - clears the list, alone");
+ if (dir_word[0] != '/' and dir_word[0] != '~')
+ return p.reportFailure(c.id, std.fmt.bufPrint(&said, "Incl: invalid directory: {s} is relative", .{shown}) catch "Incl: invalid directory");
+ var home_buf: [4096]u8 = undefined;
+ const expanded = @import("fs.zig").expandHome(dir_word, &home_buf);
+ if (comptime pardes.hosted) if (@import("fs.zig").localPath(expanded)) |local| if (!exec.isDirectory(local))
+ return p.reportFailure(c.id, std.fmt.bufPrint(&said, "Incl: no such directory: {s}", .{shown}) catch "Incl: no such directory");
+ }
var words = std.mem.tokenizeAny(u8, typed, " \t");
while (words.next()) |dir_word| {
var home_buf: [4096]u8 = undefined;
diff --git a/src/exec.zig b/src/exec.zig
index 9aa8a8cb..d6834797 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -1123,7 +1123,7 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
var said: [32]u8 = undefined;
return p.setMessage(p.active, std.fmt.bufPrint(&said, "Tab {d}", .{p.settings.tab}) catch "Tab");
} else if (!p.settings.apply(setting, arg)) {
- p.reportFailure(p.active, "Tab: takes " ++ comptime config.Runtime.takes(.tab).?);
+ p.reportFailure(p.active, "Tab: invalid tab width: takes " ++ comptime config.Runtime.takes(.tab).?);
},
// Said with what it takes: its words, its range.
else => if (!p.settings.apply(setting, arg)) {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index ae68ed91..274caf88 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2025,6 +2025,36 @@ test "Zerox twins dump their text once and come back on one buffer from a Restor
try testing.expect(std.mem.startsWith(u8, files[0].content, "one\ntwo\nunsaved "));
}
+test "a bad Tab width and a bad Incl directory are refused invalid, changing nothing; a twin's rename is each twin's" {
+ defer config.tab_now = config.tab_width;
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ for ([_][]const u8{ "Tab 0\n", "Tab 17\n", "Tab x\n" }) |line| {
+ try testing.expectEqual(E.INVAL, wr(p, root_ctl, line).errno());
+ // Run from a pane, as a tag click runs it: said invalid too.
+ const r = wr(p, Node.of(serialOf(p), .exec), line);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, "invalid tab width") != null);
+ }
+ try testing.expectEqual(config.tab_width, config.tab_now);
+ const before = p.incl.items.len;
+ try testing.expectEqual(E.INVAL, wr(p, root_ctl, "Incl rel/dir\n").errno());
+ try testing.expectEqual(E.INVAL, wr(p, root_ctl, "Incl - /usr/include\n").errno());
+ if (comptime pardes.hosted) try testing.expectEqual(E.NOENT, wr(p, root_ctl, "Incl /tmp/pardes-surely-no-such-dir\n").errno());
+ try testing.expectEqual(before, p.incl.items.len);
+ // A twin renamed: a rename record for each.
+ const first = serialOf(p);
+ try testing.expect(p.executeBuiltinLine(0, "Zerox"));
+ const twin = for (p.panes) |slot| {
+ if (slot) |pane| if (pane.serial != first and pane.file != null) break pane.serial;
+ } else return error.NoTwin;
+ _ = wr(p, Node.of(first, .name), "/tmp/pardes-twin-renamed.txt\n");
+ var rec: [64]u8 = undefined;
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "rename {d} /tmp/pardes-twin-renamed.txt\n", .{first})));
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "rename {d} /tmp/pardes-twin-renamed.txt\n", .{twin})));
+}
+
test "a body write to a terminal is typed input, not a paste, even to a program that asked for bracketed paste" {
const p = try Pardes.init(testing.allocator, .{ .cols = 90, .rows = 30 });
defer p.deinit();
diff --git a/test/fs.py b/test/fs.py
index 1303db0d..59668c57 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -627,6 +627,34 @@ def directory_panes(binary):
assert client.read(f'/pane/{serial}/body').decode().split() == ['.dot', 'later', 'sub/']
+def putall_answers_after_its_saves(binary):
+ """Putall with one pane refused (its file changed on disk under unsaved
+ edits) fails, and is answered only once the other panes' saves have
+ landed."""
+ with tempfile.TemporaryDirectory(prefix='pardes-putall-') as directory:
+ root = Path(directory).resolve()
+ (root / 'a.txt').write_bytes(b'a\n')
+ (root / 'b.txt').write_bytes(b'b\n')
+ with session(binary, root, 'putall') as (client, _):
+ a = look(client, str(root / 'a.txt'))
+ b = look(client, str(root / 'b.txt'))
+ for serial in (a, b):
+ client.write(f'/pane/{serial}/body', b'edited\n')
+ (root / 'b.txt').write_bytes(b'changed on disk\n')
+ deadline = time.monotonic() + 10
+ while f'changed {b}' not in client.read('/log').decode():
+ assert time.monotonic() < deadline, 'the watch never saw b.txt change'
+ time.sleep(.05)
+ try:
+ client.write('/ctl', b'Putall\n')
+ except OSError as why:
+ assert 'modified on disk' in str(why), why
+ else:
+ raise AssertionError('Putall over a file changed on disk was taken')
+ assert (root / 'a.txt').read_bytes() == b'a\nedited\n', (root / 'a.txt').read_bytes()
+ assert (root / 'b.txt').read_bytes() == b'changed on disk\n'
+
+
def new_terminals_named_once(binary):
"""A terminal started outside the session's directory is named by that
directory from its `new` on: no rename to the session's and back."""
@@ -1077,6 +1105,7 @@ def test(binary, quic=False):
unattended_messages_expire(binary)
new_terminals_named_once(binary)
directory_panes(binary)
+ putall_answers_after_its_saves(binary)
workflow_canary(binary)
recent_survives_restart(binary)
python_repl_runs_one_line(binary)