diff options
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 194 |
1 files changed, 163 insertions, 31 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index cca7bfb5..84904959 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -227,6 +227,8 @@ pub const registry = struct { // are the default .toggle, .transition, .scene, .dump_dir, .choice, .lift, .shader_animation, .motion, .shell => false, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .inactive_dim, .grip_width => true, + // bare, it says the width (exec.applySettingBuiltin) + .tab => false, }; unreachable; } @@ -516,7 +518,10 @@ pub const Look = struct { // name on the message row would only be noise over that pub const quiet = true; pub fn run(c: Ctx) void { - look.lookAt(c.p, c.id, c.arg orelse return); + // Bare (a key bound to it, a tag's Look with nothing held): the + // selection, else what Enter on the cursor takes, the one expansion. + const arg = c.arg orelse (exec.currentOperand(c.p, c.pane) orelse return); + look.lookAt(c.p, c.id, arg); } }; @@ -550,9 +555,9 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool { /// 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 }; +pub const Asked = union(enum) { all, pane: usize, column: usize }; -fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { +pub fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { // Every pane asked about is an `unsaved <serial> <name>` record, as // acme's rowclean names them all (wind.c:511); the refusal a write // gets names the one, or counts them, never a list cut short. @@ -570,6 +575,22 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { .column => |col| if ((layout.findPane(c.p, id) orelse continue).col != col) continue, } const f = pane.file orelse continue; + // Twins (Zerox) hold one text: closing one while another stays + // loses nothing, and the text is asked about once. + if (f.twin != 0) { + const asked_elsewhere = for (c.p.panes, 0..) |other_slot, other_id| { + const other = other_slot orelse continue; + const of = other.file orelse continue; + if (other_id == id or of.twin != f.twin) continue; + const closing = switch (which) { + .all => true, + .pane => |only| other_id == only, + .column => |col| if (layout.findPane(c.p, other_id)) |at| at.col == col else false, + }; + if (!closing or other_id < id) break true; + } else false; + if (asked_elsewhere) 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. @@ -1113,6 +1134,129 @@ pub const ClipReplace = struct { // ---- panes and columns ---- +/// acme's Get: the pane's file read again, its unsaved edits refused once +/// as Del refuses them. `Get file` loads that file into the pane instead, +/// the pane taking its name. +pub const Get = struct { + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const ctl = @import("ninep/ctl.zig"); + if (@import("edit_cmd.zig").busyOn(c.p, c.id)) return c.p.reportFailure(c.id, "Get: " ++ @import("edit_cmd.zig").e_busy); + const f = if (c.pane.file) |*file| file else return c.p.reportFailure(c.id, "Get: only a file pane takes it"); + const typed = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + // A directory pane: the directory read again (Dir.zig), as acme's + // Get is in a directory window; a file is a Look's from there. + if (f.listing != null) { + if (typed.len > 0) return c.p.reportFailure(c.id, "Get: a directory pane reads its directory; a Look at the file opens it"); + return panes.Dir.show(c.p, c.id, c.id); + } + if (!panes.Output.fileTraits(f.output).saves) return c.p.reportFailure(c.id, "Get: only a file pane takes it"); + var failed: anyerror = error.Unexpected; + var home_buf: [4096]u8 = undefined; + const path = if (typed.len == 0) c.pane.file.?.path else std.fs.path.resolvePosix(c.p.scratch.allocator(), &.{ Pardes.paneDir(c.pane), @import("fs.zig").expandHome(typed, &home_buf) }) catch return; + // A file that is not there, or a directory, is said before unsaved + // edits are asked about: there would be nothing to load over them. + if (comptime pardes.hosted) if (typed.len > 0) if (@import("fs.zig").localPath(path)) |local| { + var said: [320]u8 = undefined; + const kind = exec.kindOf(local); + if (kind == null or kind.? == .directory) + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Get: {s}: {s}", .{ path[0..@min(path.len, 256)], if (kind == null) "no such file" else "is a directory" }) catch "Get: no such file"); + }; + if (ctl.getRefused(c.p, c.pane, "Get")) |said| return c.p.reportFailure(c.id, said); + const errno = if (typed.len == 0) ctl.get(c.p, c.pane, &failed) else ctl.getFile(c.p, c.pane, path, &failed); + if (errno != 0) { + var said: [320]u8 = undefined; + var words: [128]u8 = undefined; + const why = if (failed == error.FileNotFound) "no such file" else pardes.Messages.errorWords(failed, &words); + c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Get: {s}: {s}", .{ path[0..@min(path.len, 256)], why }) catch "Get: cannot be read"); + } + } +}; + +/// acme's Putall: every pane holding unsaved edits to a file is saved, each +/// as its own Save; one that is refused (changed on disk since, a write +/// that fails) says so and the rest are saved all the same. +pub const Putall = struct { + pub const scope: Scope = .session; + pub fn run(c: Ctx) void { + for (c.p.panes, 0..) |slot, id| { + const pane = slot orelse continue; + const f = pane.file orelse continue; + if (f.output != null or f.listing != null or f.revision == f.saved_revision) continue; + // A Zerox pair is one buffer: saved once, by its first pane. + if (f.twin != 0) if (for (c.p.panes[0..id]) |earlier| { + if ((earlier orelse continue).file) |ef| if (ef.twin == f.twin) break true; + } else false) continue; + exec.saveFile(c.p, id); + } + } +}; + +/// acme's Incl, for the session: `Incl dir ...` adds directories a look at a +/// name found nowhere else is tried in (`stdio.h`, `<foo.h>`), bare lists +/// them, `Incl -` empties the list. +pub const Incl = struct { + pub const scope: Scope = .session; + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const p = c.p; + const typed = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + if (std.mem.eql(u8, typed, "-")) { + for (p.incl.items) |dir| p.gpa.free(dir); + 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, if (exec.kindOf(local) != null) + std.fmt.bufPrint(&said, "Incl: not a directory: {s}", .{shown}) catch "Incl: not a directory" + else + 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; + const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ Pardes.paneDir(c.pane), @import("fs.zig").expandHome(dir_word, &home_buf) }) catch return; + if (!std.fs.path.isAbsolute(full)) return p.reportFailure(c.id, "Incl: invalid directory"); + for (p.incl.items) |dir| { + if (std.mem.eql(u8, dir, full)) break; + } else { + const copy = p.gpa.dupe(u8, full) catch return; + p.incl.append(p.gpa, copy) catch return p.gpa.free(copy); + } + } + if (typed.len > 0) return; + var list: std.Io.Writer.Allocating = .init(p.scratch.allocator()); + list.writer.writeAll("Incl:") catch return; + for (p.incl.items) |dir| list.writer.print(" {s}", .{dir}) catch return; + if (p.incl.items.len == 0) list.writer.writeAll(" none") catch return; + p.setMessage(c.id, list.written()); + } +}; + +/// acme's Zerox: a second pane on this pane's file, the same text, undo and +/// unsaved state, with a scroll and a cursor of its own (File.zerox). +pub const Zerox = struct { + pub fn run(c: Ctx) void { + if (@import("edit_cmd.zig").busyOn(c.p, c.id)) return c.p.reportFailure(c.id, "Zerox: " ++ @import("edit_cmd.zig").e_busy); + const f = c.pane.file orelse return c.p.reportFailure(c.id, "Zerox: only a file pane takes it"); + if (f.output != null or f.listing != null or f.mini != null) return c.p.reportFailure(c.id, "Zerox: only a file pane takes it"); + const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Zerox", error.NoPaneSlots); + _ = panes.File.zerox(c.p, c.id, free) catch |err| return c.p.reportError(c.id, "Zerox", err); + exec.placeNew(c.p, c.id, c.id, free, .doc); + } +}; + /// Write the pane's text to its file, or to the file its argument names. pub const Save = struct { pub const takes_arg = true; @@ -1122,6 +1266,12 @@ pub const Save = struct { // silent success. if (c.pane.file == null and !c.pane.isTerminal()) return c.p.reportFailure(c.id, if (c.pane.hasPdf()) "Save: a PDF pane has no text to save" else "Save: an image pane has no text to save"); + // A directory's listing is no file to write, bare or to a path, as + // acme's Put does nothing in a directory window. + if (c.pane.file) |file| if (file.listing != null) { + var said: [limits.host_path_cap + 48]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Save: {s} is a directory", .{file.path[0..@min(file.path.len, limits.host_path_cap)]}) catch "Save: a directory pane has no file to save"); + }; const path = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (path.len > 0) return exec.saveTo(c.p, c.id, path); // A `+` name is a generated pane's (+Tutor), no file of its own: @@ -1154,6 +1304,7 @@ 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"); + if (@import("edit_cmd.zig").busyOn(c.p, c.id)) return c.p.reportFailure(c.id, "Del: " ++ @import("edit_cmd.zig").e_busy); // 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; @@ -1213,36 +1364,15 @@ 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. +/// open text panes, from a tag, a pane ctl or exec, starting in the pane's +/// body. One undo step in each file it changes; a failure changes nothing +/// and says why. src/edit_cmd.zig runs it; 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); - // `s/a/a/` changes nothing: no undo step, no new revision. - if (std.mem.eql(u8, new, f.content)) c.p.gpa.free(new) else { - 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); + @import("edit_cmd.zig").run(c.p, c.id, c.arg orelse ""); } }; @@ -1253,6 +1383,7 @@ pub const Edit = struct { /// limits.undo_max steps (256). pub const Undo = struct { pub fn run(c: Ctx) void { + if (@import("edit_cmd.zig").busyOn(c.p, c.id)) return c.p.reportFailure(c.id, "Undo: " ++ @import("edit_cmd.zig").e_busy); if (c.pane.file) |f| if (f.history.undo_len == 0) return c.p.setMessage(c.id, "Undo: nothing to undo"); edit.doUndo(c.p, &c.pane.body); } @@ -1261,6 +1392,7 @@ pub const Undo = struct { /// Redo the last undone edit. pub const Redo = struct { pub fn run(c: Ctx) void { + if (@import("edit_cmd.zig").busyOn(c.p, c.id)) return c.p.reportFailure(c.id, "Redo: " ++ @import("edit_cmd.zig").e_busy); if (c.pane.file) |f| if (f.history.redo_len == 0) return c.p.setMessage(c.id, "Redo: nothing to redo"); edit.doRedo(c.p, &c.pane.body); } @@ -1502,7 +1634,7 @@ pub const LocationsConfig = struct { try c.p.locations_config.write(&out.writer); try out.writer.writeByte('\n'); const content = try out.toOwnedSlice(); - const dir = if (c.pane.file) |file| std.fs.path.dirname(file.path) orelse "/" else c.pane.cwdSlice(); + const dir = if (c.pane.file) |file| panes.Dir.ofName(file.path) else c.pane.cwdSlice(); try panes.Output.fillResults(c.p, c.id, dir, .{ .cmd = .LocationsConfig }, "", content, null); } }; @@ -2082,7 +2214,7 @@ const Board = struct { p.gpa.free(content); return error.MissingPane; }; - const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); + const dir = if (pane.file) |f| panes.Dir.ofName(f.path) else pane.cwdSlice(); try panes.Output.fillResults(p, id, dir, from, "", content, null); } |
