summaryrefslogtreecommitdiff
path: root/src/edit_cmd.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/edit_cmd.zig')
-rw-r--r--src/edit_cmd.zig94
1 files changed, 81 insertions, 13 deletions
diff --git a/src/edit_cmd.zig b/src/edit_cmd.zig
index ebeae223..83cc7e4f 100644
--- a/src/edit_cmd.zig
+++ b/src/edit_cmd.zig
@@ -67,6 +67,7 @@ pub const Write = struct {
const World = struct {
p: *Pardes,
from: usize,
+ arena: std.mem.Allocator,
fn check(ctx: *anyopaque, path: []const u8, why: *sam.Why) bool {
const w: *World = @ptrCast(@alignCast(ctx));
@@ -103,7 +104,7 @@ const World = struct {
const pane = slot orelse continue;
const f = ninep_pane.fileOf(pane) orelse continue;
if (!std.mem.eql(u8, f.path, path)) continue;
- return fileOf(pane, id);
+ return fileOf(w.arena, pane, id) catch said(sam.File, why, "B: out of memory", .{});
}
return said(sam.File, why, "B: cannot open {s}", .{path});
}
@@ -113,13 +114,9 @@ const World = struct {
if (comptime pardes.hosted) if (filesystem.localPath(path)) |local| if (@import("exec.zig").isDirectory(local))
return said([]const u8, why, "{s} is a directory", .{path});
const bytes = filesystem.read(w.p, path) catch |err| {
- const name = @errorName(err);
- const words = if (std.mem.eql(u8, name, "FileNotFound"))
- "file does not exist"
- else if (std.mem.eql(u8, name, "AccessDenied") or std.mem.eql(u8, name, "PermissionDenied"))
- "permission denied"
- else
- name;
+ // In the words Get says them (builtins.Get).
+ var buf: [128]u8 = undefined;
+ const words = if (err == error.FileNotFound) "no such file" else pardes.Messages.errorWords(err, &buf);
return said([]const u8, why, "can't open {s}: {s}", .{ path, words });
};
defer w.p.gpa.free(bytes);
@@ -132,10 +129,13 @@ const World = struct {
}
};
-fn fileOf(pane: *panes.Pane, id: usize) sam.File {
+/// What the Edit sees of a pane. Its name is a copy in `arena`: the pane
+/// may be gone by the time a running Edit's end says it. Its text is the
+/// pane's own, read only while the Edit runs its commands' parse.
+fn fileOf(arena: std.mem.Allocator, pane: *panes.Pane, id: usize) !sam.File {
const f = ninep_pane.fileOf(pane).?;
return .{
- .name = f.path,
+ .name = try arena.dupe(u8, f.path),
.text = f.content,
.dot = ninep_pane.dotOf(pane),
.dirty = ninep_pane.dirtyOf(pane),
@@ -160,7 +160,7 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
for (p.panes, 0..) |slot, i| {
const q = slot orelse continue;
if (q.file == null) continue;
- files.append(arena, fileOf(q, i)) catch |err| return p.reportError(id, "Edit", err);
+ files.append(arena, fileOf(arena, q, i) catch |err| return p.reportError(id, "Edit", err)) catch |err| return p.reportError(id, "Edit", err);
}
std.mem.sort(sam.File, files.items, p, struct {
fn before(core: *Pardes, a: sam.File, b: sam.File) bool {
@@ -171,7 +171,7 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
if (f.id == id) break i;
} else unreachable;
- var world: World = .{ .p = p, .from = id };
+ var world: World = .{ .p = p, .from = id, .arena = arena };
var why: sam.Why = .{};
var text: [260]u8 = undefined;
const res = sam.run(arena, files.items, cur, command, .{ .ctx = &world, .check = World.check, .open = World.open, .read = World.read, .refuseGet = World.refuseGet }, &why) catch |err| switch (err) {
@@ -219,7 +219,7 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
for (res.jobs, 0..) |job, i| {
const f = res.files[job.file];
commands[i] = job.command;
- cwds[i] = Pardes.paneDir(p.panes[f.id].?);
+ cwds[i] = arena.dupe(u8, Pardes.paneDir(p.panes[f.id].?)) catch |err| return p.reportError(id, "Edit", err);
inputs[i] = .{ .bytes = if (job.c == '<') "" else arena.dupe(u8, f.text[job.q0..job.q1]) catch |err| return p.reportError(id, "Edit", err) };
}
p.pipe.seq +%= 1;
@@ -245,6 +245,29 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
if (p.fs.serving) p.fs.edit_started = true;
}
+/// What a pane whose Edit's commands are running refuses to do beside them:
+/// close, undo, redo, Get, Zerox (busyOn).
+pub const e_busy = "busy: an Edit's commands are running on it";
+
+/// Whether pane `id` (or a Zerox twin of it) is one the Edit whose commands
+/// are running will change or run a command on.
+pub fn busyOn(p: *Pardes, id: usize) bool {
+ const pd = p.pipe.edit_run orelse return false;
+ const pane = p.panes[id] orelse return false;
+ const twin = if (pane.file) |f| f.twin else 0;
+ for (pd.res.files, 0..) |f, i| {
+ const used = changes(f) or for (pd.res.jobs) |job| {
+ if (job.file == i) break true;
+ } else false;
+ if (!used) continue;
+ const q = p.panes[f.id] orelse continue;
+ if (q.serial != pd.serials[i]) continue;
+ if (f.id == id) return true;
+ if (twin != 0) if (q.file) |qf| if (qf.twin == twin) return true;
+ }
+ return false;
+}
+
/// Whether the Edit changes file `f`: its text, its name, or its pane.
fn changes(f: sam.File) bool {
return f.ops.items.len > 0 or f.get != null or f.close or f.renamed;
@@ -727,3 +750,48 @@ test "an Edit with no command runs beside one whose commands run, unless it touc
answer(p, &.{"a\nb\n"});
try testing.expectEqualStrings("a\nb\n", p.panes[0].?.file.?.content);
}
+
+test "a pane an Edit's commands will change is not closed under them, and one closed anyway is named, not read freed" {
+ const p = try th.withFile(testing.allocator, "b\na\n");
+ defer p.deinit();
+ const serial = th.serialOf(p);
+ const ctl = tree.Node.of(serial, .ctl);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit ,|sleep 0.8; cat\n").reply.status);
+ // Del, acme's delete, rmdir, Undo, Redo, Get, Zerox: busy, EBUSY.
+ for ([_][]const u8{ "delete\n", "Del\n", "Undo\n", "Redo\n", "get\n", "Get\n", "Zerox\n" }) |line| {
+ const busy = th.wr(p, ctl, line);
+ try testing.expectEqual(tree.E.BUSY, busy.errno());
+ try testing.expect(std.mem.indexOf(u8, busy.reply.ename, e_busy) != null);
+ }
+ try testing.expectEqual(tree.E.BUSY, th.rmdir(p, tree.Node.of(serial, .dir)).errno());
+ try testing.expect(p.paneBySerial(serial) != null);
+ // Closed by a way that does not ask (a session's own teardown of one
+ // pane): the Edit's end names it from its own copy.
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 1, .tag = 2, .node = ctl, .handle = 0, .written = 1 };
+ try p.removePane(0, null);
+ answer(p, &.{"a\nb\n"});
+ try testing.expectEqualStrings("Edit: /test.txt closed while its commands ran; nothing changed", filesystem.EditHold.answer(&p.fs).ename);
+}
+
+test "a session that ends while an Edit's commands run lets the Edit go" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(th.serialOf(p), .ctl), "Edit ,|sleep 5; cat\n").reply.status);
+ try testing.expect(p.pipe.edit_run != null);
+ p.deinit(); // the testing allocator says if anything is kept
+}
+
+test "e and r say a file that cannot be read as Get says it" {
+ if (comptime !pardes.hosted or !filesystem.platform_has_fs) return error.SkipZigTest;
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const ctl = tree.Node.of(th.serialOf(p), .ctl);
+ for ([_][2][]const u8{
+ .{ "Edit 0r /dev/zero\n", "not a regular file" },
+ .{ "Edit 0r /tmp\n", "is a directory" },
+ .{ "Edit 0r /nonexistent-pardes-file\n", "no such file" },
+ }) |c| {
+ const r = th.wr(p, ctl, c[0]);
+ try testing.expect(r.reply.status == .err);
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, c[1]) != null);
+ }
+}