summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig117
1 files changed, 103 insertions, 14 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index c25311e3..657f49d0 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -171,7 +171,7 @@ pub const registry = struct {
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, .lift => false,
+ .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,
};
unreachable;
@@ -242,6 +242,28 @@ test "Exit asks once about unsaved text, and quits when asked again" {
try std.testing.expect(p.quit);
}
+test "Exit over a ctl write names every pane it asks about, and the next Exit discards them all" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const first = try p.setTestFile("saved\n");
+ first.file.?.saved_revision = first.file.?.revision -% 1;
+ p.newScratchBelow(p.active);
+ const second = p.panes[p.active].?;
+ panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "x" ** 120));
+ try std.testing.expect(second.file.?.revision != second.file.?.saved_revision);
+ 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, 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)"));
+ p.fs.failure_len = 0;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(p.quit);
+}
+
/// A dump file that exists, for a Restore to get past reading it.
fn testDump(buf: []u8) ![]const u8 {
var tmp = std.testing.tmpDir(.{});
@@ -421,6 +443,10 @@ pub const Exec = struct {
/// worth the question, as winclean does not ask for one.
fn warnModified(c: Ctx, asking: Pane.Discarding) 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).
+ var all_buf: [256]u8 = undefined;
+ var all = std.Io.Writer.fixed(&all_buf);
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
const f = pane.file orelse continue;
@@ -430,8 +456,15 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
pane.discard_warned = .{ .revision = f.revision, .by = asking };
var buf: [limits.host_path_cap + 48]u8 = undefined;
c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ f.path, @tagName(asking) }) catch f.path);
+ all.print("{s}{s}", .{ if (modified) ", " else "", f.path }) catch {};
modified = true;
}
+ if (modified and c.p.fs.no_prompt) {
+ all.print(": Modified ({s} again to discard them all)", .{@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);
+ }
return modified;
}
@@ -787,11 +820,11 @@ pub const New = struct {
}
};
-/// The same empty scratch, opened in a fresh column beside the calling pane.
+/// An empty column right of the keyboard's, its tag taking the keyboard.
pub const Newcol = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
- c.p.newScratchColumn(c.id);
+ c.p.newColumn(c.id);
}
};
@@ -823,6 +856,8 @@ pub const Del = struct {
{
c.pane.prompt = .del_side;
c.p.active = c.id;
+ var rec: [32]u8 = undefined;
+ pardes.ctlfs.events.notePath(c.p, "ask", std.fmt.bufPrint(&rec, "{d} del k j", .{c.pane.serial}) catch "");
// The question says Del already.
if (c.pane.msg_announcement) c.pane.msg_len = 0;
return;
@@ -852,6 +887,51 @@ pub const Togglettymode = struct {
}
};
+/// acme's Edit (exec.c `edit`, edit.c): sam's command language over the
+/// pane's body, from a tag, a pane ctl or exec. One undo step; a failure
+/// changes nothing and says why. src/sam_edit.zig has what is left out.
+pub const Edit = struct {
+ pub const takes_arg = true;
+ pub const requires_arg = true;
+ pub fn run(c: Ctx) void {
+ const sam = @import("sam_edit.zig");
+ const ninep_pane = @import("ninep/pane.zig");
+ const f = ninep_pane.fileOf(c.pane) orelse return c.p.reportFailure(c.id, "Edit: only a text pane is edited");
+ const arena = c.p.scratch.allocator();
+ var why: sam.Why = .{};
+ var said: [260]u8 = undefined;
+ const res = sam.run(arena, f.content, ninep_pane.dotOf(c.pane), f.path, c.arg orelse "", &why) catch |err| switch (err) {
+ error.Edit => return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Edit: {s}", .{why.text()}) catch "Edit: failed"),
+ error.OutOfMemory => return c.p.reportError(c.id, "Edit", err),
+ };
+ if (res.undo != 0) {
+ for (0..@abs(res.undo)) |_| if (res.undo > 0) panes.File.undo(c.p, c.pane) else panes.File.redo(c.p, c.pane);
+ return;
+ }
+ if (res.ops.len > 0) {
+ const new = sam.apply(c.p.gpa, f.content, res.ops) catch |err| return c.p.reportError(c.id, "Edit", err);
+ panes.File.pushUndo(c.p, c.pane);
+ panes.File.setContent(c.p, f, new);
+ }
+ ninep_pane.setDot(c.pane, sam.moveDot(res.dot, res.ops));
+ if (res.printed.len > 0) _ = ninep_pane.appendErrors(c.p, c.id, res.printed);
+ }
+};
+
+/// acme's Undo and Redo (exec.c `undo`): step the pane's body back or
+/// forward through its edits, from a tag, a pane ctl or exec.
+pub const Undo = struct {
+ pub fn run(c: Ctx) void {
+ edit.doUndo(c.p, &c.pane.body);
+ }
+};
+
+pub const Redo = struct {
+ pub fn run(c: Ctx) void {
+ edit.doRedo(c.p, &c.pane.body);
+ }
+};
+
/// Fold a pane to its tag, or restore its share of the column.
pub const Collapse = struct {
pub fn run(c: Ctx) void {
@@ -861,7 +941,10 @@ pub const Collapse = struct {
pub const Delcol = struct {
pub fn run(c: Ctx) void {
- c.p.removeColumn(c.id) catch |err| c.p.reportError(c.id, "close column", err);
+ // 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;
+ c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err);
}
};
@@ -973,9 +1056,9 @@ pub const Joincol = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
// The keyboard's column joins the one to its right; the last has none.
- const at = layout.findPane(c.p, c.p.active) orelse return;
- if (at.col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right");
- layout.joinCol(c.p);
+ const col = c.p.keyboardColumn(c.p.active) orelse return;
+ if (col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right");
+ layout.joinCol(c.p, col);
}
};
@@ -983,8 +1066,8 @@ pub const Tutor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
- const nt = c.p.openTutorView(free) catch return;
- exec.placeDoc(c.p, c.id, free, nt); // a doc like any other
+ _ = c.p.openTutorView(free) catch return;
+ exec.placeNew(c.p, c.id, c.id, free, .doc); // a doc like any other
}
};
@@ -1097,8 +1180,10 @@ pub const Find = struct {
pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .find, .top) catch |err|
- c.p.reportError(c.id, "find", err);
+ if (pat.len > 0) {
+ _ = look.runSearch(c.p, c.id, pat, .find, .top, true) catch |err| c.p.reportError(c.id, "find", err);
+ return;
+ }
c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.find_marker });
}
};
@@ -1111,8 +1196,10 @@ pub const Grep = struct {
pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true, .locations = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .grep, .top) catch |err|
- c.p.reportError(c.id, "grep", err);
+ if (pat.len > 0) {
+ _ = look.runSearch(c.p, c.id, pat, .grep, .top, true) catch |err| c.p.reportError(c.id, "grep", err);
+ return;
+ }
c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.grep_marker });
}
};
@@ -1141,8 +1228,10 @@ pub const Down = struct {
return tagline.enterHeader(c.p, if (column) |at| at.col else 0);
// Onto the top pane of the column the header stands over.
const col = c.p.header_column orelse if (column) |at| at.col else return;
+ // An empty column's tag has nothing below it.
+ if (col < c.p.ncol and c.p.col_n[col] == 0) return;
tagline.exitHeader(c.p);
- if (col < c.p.ncol and c.p.col_n[col] > 0) c.p.active = c.p.col_panes[col][0];
+ if (col < c.p.ncol) c.p.active = c.p.col_panes[col][0];
return;
}
_ = layout.focusDir(c.p, c.id, .down);