summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig194
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);
}