summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig91
1 files changed, 82 insertions, 9 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 657f49d0..32e3cd84 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -170,9 +170,9 @@ pub const registry = struct {
if (@intFromEnum(b) == i) return @hasDecl(T, "requires_arg") and T.requires_arg;
inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
if (@intFromEnum(b) == i) return switch (setting.action) {
- // a switch flips bare, and DumpDir bare is the default
- .toggle, .transition, .scene, .dump_dir, .choice, .lift => false,
- .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim, .grip_width => true,
+ // a switch flips bare, a choice steps, and DumpDir bare is the default
+ .toggle, .transition, .scene, .dump_dir, .choice, .lift, .shader_animation, .motion => false,
+ .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .inactive_dim, .grip_width => true,
};
unreachable;
}
@@ -258,7 +258,7 @@ test "Exit over a ctl write names every pane it asks about, and the next Exit di
const said = p.fs.failure[0..p.fs.failure_len];
try std.testing.expect(std.mem.indexOf(u8, said, first.file.?.path) != null);
try std.testing.expect(std.mem.indexOf(u8, said, second.file.?.path) != null);
- try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard them all)"));
+ try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard)"));
p.fs.failure_len = 0;
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(p.quit);
@@ -267,7 +267,11 @@ test "Exit over a ctl write names every pane it asks about, and the next Exit di
/// A dump file that exists, for a Restore to get past reading it.
fn testDump(buf: []u8) ![]const u8 {
var tmp = std.testing.tmpDir(.{});
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = ".{}" });
+ // A real dump: a Restore reads it through before it asks anything.
+ const source = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer source.deinit();
+ try pardes.dump.dumpState(source);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = source.dump_out.? });
var dir: [4096]u8 = undefined;
const at = dir[0..try tmp.dir.realPath(std.testing.io, &dir)];
return std.fmt.bufPrint(buf, "Restore {s}/d.zon", .{at});
@@ -442,6 +446,14 @@ pub const Exec = struct {
/// the person at the keyboard. A scratch of under 100 bytes is no loss
/// worth the question, as winclean does not ask for one.
fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
+ return warnModifiedIn(c, asking, .all);
+}
+
+/// Which panes a refusal asks about: every one (Exit, Restore), one (Del),
+/// or a column's (Delcol).
+const Asked = union(enum) { all, pane: usize, column: usize };
+
+fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool {
var modified = false;
// Every pane asked about is named in the one refusal a ctl write gets,
// as acme's rowclean names them all (wind.c:511).
@@ -449,9 +461,23 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
var all = std.Io.Writer.fixed(&all_buf);
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
+ switch (which) {
+ .all => {},
+ .pane => |only| if (id != only) continue,
+ .column => |col| if ((layout.findPane(c.p, id) orelse continue).col != col) continue,
+ }
const f = pane.file orelse continue;
- if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue;
+ // A +New scratch saves nowhere, but what is typed in it is as lost;
+ // under 100 bytes it is not asked about, as acme's winclean does not
+ // whine about a small unnamed window, nor about a command's output.
+ const scratch = if (f.output) |o| o.from == .cmd and o.from.cmd == .New else false;
+ if (f.revision == f.saved_revision or !(panes.Output.fileTraits(f.output).saves or scratch)) continue;
if (f.output != null and f.content.len < 100) continue;
+ // Dirty by a rename alone is no text to lose, as get says (ctl.zig).
+ // Nor is an empty buffer that never held a file.
+ if (f.saved_hash) |saved| {
+ if (std.hash.Wyhash.hash(0, f.content) == saved) continue;
+ } else if (f.content.len == 0) continue;
if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == asking) continue;
pane.discard_warned = .{ .revision = f.revision, .by = asking };
var buf: [limits.host_path_cap + 48]u8 = undefined;
@@ -460,7 +486,7 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
modified = true;
}
if (modified and c.p.fs.no_prompt) {
- all.print(": Modified ({s} again to discard them all)", .{@tagName(asking)}) catch {};
+ all.print(": Modified ({s} again to discard)", .{@tagName(asking)}) catch {};
const said = @import("Messages.zig").clip(all.buffered(), c.p.fs.failure.len);
@memcpy(c.p.fs.failure[0..said.len], said);
c.p.fs.failure_len = @intCast(said.len);
@@ -558,7 +584,12 @@ pub const Restore = struct {
};
return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: {s}", .{ path, why }) catch "Restore: cannot read the dump");
};
- c.p.gpa.free(bytes);
+ defer c.p.gpa.free(bytes);
+ // Nor does a file that is not a dump, before any warning.
+ pardes.dump.check(c.p.gpa, bytes) catch |err| {
+ var buf: [limits.host_path_cap + 64]u8 = undefined;
+ return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: not a pardes dump ({s})", .{ path, @errorName(err) }) catch "Restore: not a pardes dump");
+ };
}
// acme's Load adds a dump's windows to the ones there; a Restore
// replaces them all, so it asks what Exit asks first.
@@ -835,6 +866,9 @@ pub const Del = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
const side = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
+ // acme's Del asks winclean first (exec.c del): unsaved text is warned
+ // about once, and the same Del again, nothing edited since, closes.
+ if (warnModifiedIn(c, .Del, .{ .pane = c.id })) return;
const at = layout.findPane(c.p, c.id);
const above = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .above) else null;
const below = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .below) else null;
@@ -944,6 +978,9 @@ pub const Delcol = struct {
// The column of the tag it came from, else the pane's: a pane ctl's
// Delcol closes that pane's column wherever the keyboard is.
const col = c.p.exec_column orelse (layout.findPane(c.p, c.id) orelse return).col;
+ // acme's Delcol refuses a column with unsaved text (exec.c delcol,
+ // cols.c colclean), once: the next Delcol closes it.
+ if (warnModifiedIn(c, .Delcol, .{ .column = col })) return;
c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err);
}
};
@@ -1065,7 +1102,7 @@ pub const Joincol = struct {
pub const Tutor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
- const free = c.p.freeSlot() orelse return;
+ const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Tutor", error.NoPaneSlots);
_ = c.p.openTutorView(free) catch return;
exec.placeNew(c.p, c.id, c.id, free, .doc); // a doc like any other
}
@@ -1724,3 +1761,39 @@ const Board = struct {
try std.testing.expect(20 != 0x20);
}
};
+
+test "Exit and Restore name a modified +New scratch as well as files" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const file = try p.setTestFile("saved\n");
+ file.file.?.saved_revision = file.file.?.revision -% 1; // modified
+ p.newScratchBelow(p.active);
+ const scratch = &p.panes[p.active].?.file.?;
+ scratch.content = try p.gpa.realloc(scratch.content, 120);
+ @memset(scratch.content, 'n'); // past acme's 100 bytes, where it whines
+ scratch.revision +%= 1; // typed into, never saved
+ p.fs.no_prompt = true;
+ defer p.fs.no_prompt = false;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(!p.quit);
+ const said = p.fs.failure[0..p.fs.failure_len];
+ try std.testing.expect(std.mem.indexOf(u8, said, "test.txt") != null);
+ try std.testing.expect(std.mem.indexOf(u8, said, config.scratch_buffer) != null);
+}
+
+test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("saved\n");
+ pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ const serial = pane.serial;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Delcol again to discard)"));
+ // Each word is warned on its own: Del asks too, then closes.
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Del again to discard)"));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) == null);
+}