diff options
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 117 |
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); |
