summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig265
1 files changed, 208 insertions, 57 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 32e3cd84..3f4a680c 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -170,9 +170,10 @@ 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, 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,
+ // a switch flips bare, a choice steps, and DumpDir and Shell bare
+ // 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,
};
unreachable;
}
@@ -234,6 +235,7 @@ test "Exit asks once about unsaved text, and quits when asked again" {
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(!p.quit);
try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Exit again to discard)"));
@@ -242,26 +244,41 @@ 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)"));
- p.fs.failure_len = 0;
- try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
- try std.testing.expect(p.quit);
+test "Exit over a ctl write or a click names every pane it asks about, a second only those edited since, and the next discards them all" {
+ for ([_]bool{ false, true }) |clicked| {
+ 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;
+ first.file.?.saved_hash = 0; // edited: what it had is not what it has
+ 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);
+ // A ctl write, or a click through exec (ctl.captured).
+ if (clicked) p.fs.capturing = true else p.fs.no_prompt = true;
+ defer p.fs.capturing = false;
+ defer p.fs.no_prompt = false;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(!p.quit);
+ // Two: counted, never a list cut short; `unsaved` records name them.
+ try std.testing.expectEqualStrings("2 unsaved panes: Modified (Exit again to discard)", p.fs.failure[0..p.fs.failure_len]);
+ const log = p.fs.log.buf.items;
+ var rec: [64]u8 = undefined;
+ for ([_]*const Pane{ first, second }) |pane|
+ try std.testing.expect(std.mem.indexOf(u8, log, try std.fmt.bufPrint(&rec, "unsaved {d} ", .{pane.serial})) != null);
+ // Edited since: only that one is named again (acme's rule).
+ p.fs.failure_len = 0;
+ panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "y" ** 120));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(!p.quit);
+ const again = p.fs.failure[0..p.fs.failure_len];
+ try std.testing.expect(std.mem.indexOf(u8, again, first.file.?.path) == null);
+ try std.testing.expect(std.mem.startsWith(u8, again, second.file.?.path));
+ 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.
@@ -282,6 +299,7 @@ test "Restore asks about unsaved text as Exit does, and restores when asked agai
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
var line_buf: [4200]u8 = undefined;
const line = try testDump(&line_buf);
try std.testing.expect(p.executeBuiltinLine(p.active, line));
@@ -296,6 +314,7 @@ test "a Restore of a dump that cannot be read says so, before the unsaved-text w
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
p.fs.no_prompt = true;
defer p.fs.no_prompt = false;
try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /nope/missing.zon"));
@@ -310,6 +329,7 @@ test "a Restore's warning is not an Exit's: each word is warned on its own" {
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
var line_buf: [4200]u8 = undefined;
try std.testing.expect(p.executeBuiltinLine(p.active, try testDump(&line_buf)));
try std.testing.expect(p.restore_req == null);
@@ -394,7 +414,7 @@ test "Ligatures is a setting only in the shell that shapes text" {
.startup_config_path = null,
.platform = @tagName(pardes.platform),
.theme_name = p.theme().name,
- .compiled_default_shell = config.default_shell,
+ .compiled_default_shell = config.defaultShell(),
.hover_delay_frames = null,
.native_images = false,
.capabilities = capabilities,
@@ -413,6 +433,7 @@ comptime {
// ---- the two acme verbs ----
+/// Look at its argument: open the file or address it names, else find the word, as a right click does.
pub const Look = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -424,6 +445,7 @@ pub const Look = struct {
}
};
+/// Execute its argument as a middle click on it would, a builtin or a shell command.
pub const Exec = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -454,11 +476,12 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
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).
- var all_buf: [256]u8 = undefined;
- var all = std.Io.Writer.fixed(&all_buf);
+ // 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.
+ var count: usize = 0;
+ var one_buf: [limits.host_path_cap + 24]u8 = undefined;
+ var one: []const u8 = "";
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
switch (which) {
@@ -474,28 +497,39 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool {
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;
+ // Nor is a buffer never edited at all: its first edit is what keeps
+ // the text it had (File.zig), so with none there is nothing to lose.
+ const saved = f.saved_hash orelse continue;
+ if (std.hash.Wyhash.hash(0, f.content) == saved) 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;
- 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;
+ // Scratches share a name: each is named with its pane's serial too,
+ // `/dir/+New (pane 12)`, the same in every refusal (Exit, Restore,
+ // Del, Delcol).
+ var name_buf: [limits.host_path_cap + 24]u8 = undefined;
+ const name = if (scratch) std.fmt.bufPrint(&name_buf, "{s} (pane {d})", .{ f.path, pane.serial }) catch f.path else f.path;
+ var buf: [limits.host_path_cap + 72]u8 = undefined;
+ pardes.ctlfs.events.noteLog(c.p, .unsaved, pane);
+ c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ name, @tagName(asking) }) catch name);
+ if (count == 0) one = std.fmt.bufPrint(&one_buf, "{s}", .{name}) catch name;
+ count += 1;
}
- if (modified and c.p.fs.no_prompt) {
- 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);
+ if (count > 0 and (c.p.fs.no_prompt or c.p.fs.capturing)) {
+ var said_buf: [limits.host_path_cap + 96]u8 = undefined;
+ const said = (if (count == 1)
+ std.fmt.bufPrint(&said_buf, "{s}: Modified ({s} again to discard)", .{ one, @tagName(asking) })
+ else
+ std.fmt.bufPrint(&said_buf, "{d} unsaved panes: Modified ({s} again to discard)", .{ count, @tagName(asking) })) catch "unsaved panes: Modified";
+ const kept = @import("Messages.zig").clip(said, c.p.fs.failure.len);
+ @memcpy(c.p.fs.failure[0..kept.len], kept);
+ c.p.fs.failure_len = @intCast(kept.len);
}
- return modified;
+ return count > 0;
}
-/// acme's Exit (editors/acme/exec.c:892): quit, once warnModified has
-/// nothing to say.
+/// Quit the editor, refusing once while a pane holds unsaved text: an Exit
+/// with nothing edited since discards it (acme's Exit, editors/acme/exec.c:892,
+/// asking warnModified first).
pub const Exit = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
@@ -557,6 +591,7 @@ pub const Kill = struct {
}
};
+/// Write the session's panes and layout to a dump file, for Restore.
pub const Dump = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
@@ -564,6 +599,7 @@ pub const Dump = struct {
}
};
+/// Replace every pane with those of a dump file, the last one Dump wrote when bare.
pub const Restore = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -599,6 +635,7 @@ pub const Restore = struct {
}
};
+/// Attach this terminal to a detached pardes session by name.
pub const Attach = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -614,6 +651,7 @@ pub const Attach = struct {
}
};
+/// Detach this pardes from its terminal, leaving the session running.
pub const Detach = struct {
pub const scope: Scope = .session;
pub const enabled = pardes.can_attach;
@@ -625,6 +663,7 @@ pub const Detach = struct {
}
};
+/// Mount another pardes's 9P tree under /n/<name>, given its dial address.
pub const Mount = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -637,9 +676,25 @@ pub const Mount = struct {
const dial = std.mem.trim(u8, args.rest(), " \t");
if (dial.len == 0) return c.p.reportError(c.id, "Mount name dial", error.MissingArgument);
c.p.fs.mount(c.p.gpa, name, dial) catch |err| return c.p.reportError(c.id, "Mount", err);
+ // A mount that cannot be reached is said now, and taken back, not
+ // found out by the first look through it.
+ const reached = reached: {
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ break :reached @import("9p_io.zig").Client.probe(c.p.gpa, dial);
+ };
+ reached catch |err| {
+ if (c.p.fs.mounts.pop()) |gone| {
+ c.p.gpa.free(gone.name);
+ c.p.gpa.free(gone.dial);
+ }
+ var what: [160]u8 = undefined;
+ return c.p.reportError(c.id, std.fmt.bufPrint(&what, "Mount {s} {s}", .{ name, dial[0..@min(dial.len, 100)] }) catch "Mount", err);
+ };
}
};
+/// Unmount the 9P tree mounted under /n/<name>.
pub const Unmount = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -656,6 +711,7 @@ pub const Unmount = struct {
// ---- the message row ----
+/// Show its argument on this pane's message row.
pub const Msg = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -668,6 +724,7 @@ pub const Msg = struct {
}
};
+/// Switch to the next bundled theme.
pub const NextColor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
@@ -675,6 +732,7 @@ pub const NextColor = struct {
}
};
+/// List the themes in a pane, each a word to click.
pub const ThemeSel = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .commands = true };
@@ -683,6 +741,7 @@ pub const ThemeSel = struct {
}
};
+/// Load a theme from a file.
pub const ThemeFile = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -696,6 +755,7 @@ pub const ThemeFile = struct {
}
};
+/// Write the bundled themes into the config directory's themes, to edit.
pub const DumpThemes = struct {
pub const scope: Scope = .session;
pub const enabled = pardes.hosted;
@@ -710,6 +770,7 @@ pub const DumpThemes = struct {
}
};
+/// List the fonts in a pane, each a word to click.
pub const FontSel = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true };
@@ -722,6 +783,7 @@ pub const FontSel = struct {
}
};
+/// Show the declarations enclosing the top row at the pane's head, on or off.
pub const TreeContext = struct {
pub const enabled = pardes.syntax.enabled;
pub const takes_arg = true;
@@ -746,6 +808,7 @@ pub const TreeContext = struct {
}
};
+/// Fit the PDF page to the pane by width or by height, flipping between them.
pub const PdfFit = struct {
pub const enabled = pardes.pdf_enabled;
pub fn run(c: Ctx) void {
@@ -756,6 +819,7 @@ pub const PdfFit = struct {
}
};
+/// Tint the PDF page to the theme, or show it as printed, flipping between them.
pub const PdfTint = struct {
pub const enabled = pardes.pdf_enabled;
pub fn run(c: Ctx) void {
@@ -766,6 +830,7 @@ pub const PdfTint = struct {
}
};
+/// List the PDF's sections in a pane, each a place to go.
pub const PdfSections = struct {
pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true, .locations = true };
pub const enabled = pardes.pdf_enabled;
@@ -800,6 +865,7 @@ pub const Ascii = struct {
// ---- the system clipboard ----
+/// Copy the selection to the system clipboard.
pub const ClipYank = struct {
pub fn run(c: Ctx) void {
edit.clipYank(c.p, c.pane, false);
@@ -814,18 +880,21 @@ pub const ClipYankMain = struct {
}
};
+/// Paste the system clipboard after the selection.
pub const ClipPaste = struct {
pub fn run(c: Ctx) void {
edit.clipRequest(c.p, c.id, .after);
}
};
+/// Paste the system clipboard before the selection.
pub const ClipPasteBefore = struct {
pub fn run(c: Ctx) void {
edit.clipRequest(c.p, c.id, .before);
}
};
+/// Replace the selection with the system clipboard.
pub const ClipReplace = struct {
pub fn run(c: Ctx) void {
edit.clipRequest(c.p, c.id, .replace);
@@ -834,6 +903,7 @@ pub const ClipReplace = struct {
// ---- panes and columns ----
+/// Write the pane's text to its file, or to the file its argument names.
pub const Save = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
@@ -844,6 +914,7 @@ pub const Save = struct {
}
};
+/// Open an empty scratch pane, <dir>/+New, in this pane's directory.
pub const New = struct {
pub const output: OutputTraits = .{ .name = config.scratch_buffer, .doc = true, .saves = true };
pub fn run(c: Ctx) void {
@@ -900,6 +971,7 @@ pub const Del = struct {
}
};
+/// Recolour a terminal's program colours to the theme, or show them as sent, flipping between them.
pub const Filter = struct {
pub fn run(c: Ctx) void {
if (!c.pane.isTerminal()) return;
@@ -954,14 +1026,20 @@ pub const Edit = struct {
/// 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.
+/// With no step left, Undo and Redo say so and change nothing; a ctl write
+/// of them still succeeds, as acme's Undo is silent there. A file keeps
+/// limits.undo_max steps (256).
pub const Undo = struct {
pub fn run(c: Ctx) void {
+ 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);
}
};
+/// Redo the last undone edit.
pub const Redo = struct {
pub fn run(c: Ctx) void {
+ 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);
}
};
@@ -973,6 +1051,7 @@ pub const Collapse = struct {
}
};
+/// Close this column and its panes, refusing once over unsaved text.
pub const Delcol = struct {
pub fn run(c: Ctx) void {
// The column of the tag it came from, else the pane's: a pane ctl's
@@ -985,12 +1064,14 @@ pub const Delcol = struct {
}
};
+/// Close the pane, giving its rows to the pane above it.
pub const DelAbove = struct {
pub fn run(c: Ctx) void {
Del.run(.{ .p = c.p, .pane = c.pane, .id = c.id, .txt = c.txt, .arg = "k" });
}
};
+/// Close the pane, giving its rows to the pane below it.
pub const DelBelow = struct {
pub fn run(c: Ctx) void {
Del.run(.{ .p = c.p, .pane = c.pane, .id = c.id, .txt = c.txt, .arg = "j" });
@@ -1009,10 +1090,10 @@ pub const Tty = struct {
// A shell that is not there is said, where the host would quietly
// start its fallback in its place.
if (comptime pardes.hosted) if (arg.len > 0) {
- var buf: [std.fs.max_path_bytes]u8 = undefined;
- if (@import("host_io.zig").Shell.find(arg, &buf) == null) {
- var said: [320]u8 = undefined;
- return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Tty: no shell \"{s}\" (a name on the usual paths, or a path to one)", .{arg[0..@min(arg.len, 200)]}) catch "Tty: no such shell");
+ var why: [320]u8 = undefined;
+ if (@import("host_io.zig").Shell.refusal(arg[0..@min(arg.len, 200)], &why)) |refused| {
+ var said: [340]u8 = undefined;
+ return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Tty: {s}", .{refused}) catch "Tty: no such shell");
}
};
const pane = exec.spawnTty(c.p, c.id) orelse return;
@@ -1028,8 +1109,8 @@ pub const Tty9p = struct {
}
};
-/// Fold the active pane's column into the one on its right, keeping its panes.
-/// Binds this terminal as a language's REPL, by the language's name as the
+/// Binds this terminal as a language's REPL, so an exec on that language's
+/// files is typed into it instead of run. The language is named as the
/// syntax table has it: an exec made on the body of a file in that
/// language -- a middle click, the execute key, a single word like `ls`
/// as much as a selection -- and one made on this terminal's own body, is
@@ -1064,10 +1145,25 @@ pub const Repl = struct {
// Named as a code fence names it: `py`, `Python`, `sh`.
const named: ?u8 = if (manifest.byName(arg)) |i| if (grammars[i].exts.len > 0) @intCast(i) else null else null;
const lang: u8 = named orelse {
+ // A few, whole, and where the rest are: a list cut to fit the
+ // row would end mid-word.
var w = std.Io.Writer.fixed(&buf);
- w.print("Repl: no language \"{s}\"; - or one of:", .{@import("Messages.zig").clip(arg, 24)}) catch {};
- for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break;
- return c.p.reportFailure(c.id, @import("Messages.zig").clip(w.buffered(), 256));
+ w.print("Repl: no language \"{s}\"; - or one like", .{@import("Messages.zig").clip(arg, 24)}) catch {};
+ // Those that start as the word does first, a misspelling's
+ // likeliest meaning (pithon: python).
+ var shown: usize = 0;
+ var known: usize = 0;
+ for ([2]bool{ true, false }) |alike| for (grammars) |g| if (g.exts.len > 0) {
+ const starts = arg.len > 0 and g.name[0] == std.ascii.toLower(arg[0]);
+ if (starts != alike) continue;
+ known += 1;
+ if (shown < 6) {
+ w.print(" {s}", .{g.name}) catch {};
+ shown += 1;
+ }
+ };
+ w.print(" ({d} more: docs/tags.md, Repl)", .{known - shown}) catch {};
+ return c.p.reportFailure(c.id, w.buffered());
};
if (pane.repl) |r| if (r.lang == lang)
return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl");
@@ -1088,6 +1184,7 @@ pub const Repl = struct {
}
};
+/// Fold the active pane's column into the one on its right, keeping its panes.
/// The horizontal mirror of the vertical stacking `New` does.
pub const Joincol = struct {
pub const scope: Scope = .session;
@@ -1099,6 +1196,7 @@ pub const Joincol = struct {
}
};
+/// Open the tutor, a document to work through.
pub const Tutor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
@@ -1108,6 +1206,7 @@ pub const Tutor = struct {
}
};
+/// Open the help: every key and every builtin.
pub const Help = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.help_buffer };
@@ -1116,6 +1215,7 @@ pub const Help = struct {
}
};
+/// Open the settings in effect, each a word to click.
pub const Config = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.config_buffer };
@@ -1124,6 +1224,7 @@ pub const Config = struct {
}
};
+/// Set how location rows are drawn, or show how they are when bare.
pub const LocationsConfig = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1154,6 +1255,7 @@ pub const LocationsConfig = struct {
}
};
+/// Open the messages shown so far.
pub const Messages = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.messages_buffer };
@@ -1172,6 +1274,7 @@ pub const Changelog = struct {
}
};
+/// Open the source of a panel transition or post shader, by name.
pub const EffectCode = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1199,6 +1302,7 @@ pub const Pet = struct {
}
};
+/// Open a minimap of the file in braille dots.
pub const Mini = struct {
pub const takes_arg = true;
pub const output: OutputTraits = .{ .name = "Mini", .doc = true };
@@ -1211,6 +1315,7 @@ pub const Mini = struct {
// ---- search ----
+/// List every place a pattern matches in the pane, in a +Search.
pub const Find = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -1247,6 +1352,7 @@ pub const Grep = struct {
// nothing above it reaches its column's tag, then the workspace's, and Down
// comes back the same way. Left and Right walk the column tags.
+/// Move the keyboard to the pane, or column tag, to the left.
pub const Left = struct {
pub fn run(c: Ctx) void {
if (c.p.header_focus) {
@@ -1257,6 +1363,7 @@ pub const Left = struct {
}
};
+/// Move the keyboard to the pane below, off a tag onto the pane under it.
pub const Down = struct {
pub fn run(c: Ctx) void {
if (c.p.header_focus) {
@@ -1275,6 +1382,7 @@ pub const Down = struct {
}
};
+/// Move the keyboard to the pane above, and from a column's top pane to its tag.
pub const Up = struct {
pub fn run(c: Ctx) void {
if (c.p.header_focus) {
@@ -1288,6 +1396,7 @@ pub const Up = struct {
}
};
+/// Move the keyboard to the pane, or column tag, to the right.
pub const Right = struct {
pub fn run(c: Ctx) void {
if (c.p.header_focus) {
@@ -1316,6 +1425,7 @@ pub const Forward = struct {
}
};
+/// Go back to the last place jumped from in another pane.
pub const Last = struct {
pub fn run(c: Ctx) void {
var i = c.p.njumps;
@@ -1331,6 +1441,7 @@ pub const Last = struct {
}
};
+/// List the places jumped from, each a place to go.
pub const Jumplist = struct {
pub const scope: Scope = .session;
pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true, .locations = true };
@@ -1341,66 +1452,77 @@ pub const Jumplist = struct {
// ---- the language group ----
+/// Ask the language server about the symbol under the cursor.
pub const Hover = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .hover, "");
}
};
+/// List the language server's code actions for the cursor.
pub const CodeAction = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .code_action, "");
}
};
+/// Select every reference to the symbol under the cursor.
pub const SelectRefs = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .select_refs, "");
}
};
+/// List the file's symbols from the language server.
pub const Symbols = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .document_symbols, "");
}
};
+/// List the file's diagnostics from the language server.
pub const Diagnostics = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .diagnostics, "");
}
};
+/// List the workspace's diagnostics from the language server.
pub const WsDiagnostics = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .workspace_diagnostics, "");
}
};
+/// List the callers of the function under the cursor.
pub const Callers = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .incoming_calls, "");
}
};
+/// List the functions the function under the cursor calls.
pub const Callees = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .outgoing_calls, "");
}
};
+/// List the supertypes of the type under the cursor.
pub const Supertypes = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .supertypes, "");
}
};
+/// List the subtypes of the type under the cursor.
pub const Subtypes = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .subtypes, "");
}
};
+/// Rename the symbol under the cursor everywhere, to its argument.
pub const Rename = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -1411,6 +1533,7 @@ pub const Rename = struct {
}
};
+/// Search the workspace's symbols for its argument.
pub const WsSymbols = struct {
pub const takes_arg = true;
pub const requires_arg = true;
@@ -1421,18 +1544,21 @@ pub const WsSymbols = struct {
}
};
+/// Show which language servers serve this file and their state.
pub const Lspinfo = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .status, "");
}
};
+/// Explain how the last language server request was answered, step by step.
pub const Lspwhy = struct {
pub fn run(c: Ctx) void {
c.p.lspRequest(c.id, .explain, "");
}
};
+/// Read the board memory at an address, into a pane.
pub const Peek = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1447,6 +1573,7 @@ pub const Peek = struct {
}
};
+/// Write a value to the board memory at an address.
pub const Poke = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1460,6 +1587,7 @@ pub const Poke = struct {
}
};
+/// Dump a range of the board memory in hex, into a pane.
pub const Hexdump = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1474,6 +1602,7 @@ pub const Hexdump = struct {
}
};
+/// Read or set the board GPIO pins, into a pane.
pub const Gpio = struct {
pub const scope: Scope = .session;
pub const takes_arg = true;
@@ -1767,18 +1896,28 @@ test "Exit and Restore name a modified +New scratch as well as files" {
defer p.deinit();
const file = try p.setTestFile("saved\n");
file.file.?.saved_revision = file.file.?.revision -% 1; // modified
+ file.file.?.saved_hash = 0; // edited: what it had is not what it has
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
+ scratch.saved_hash = 0; // edited: what it had is not what it has
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);
+ // Each pane's message row names it; the write's refusal counts them.
+ try std.testing.expectEqualStrings("2 unsaved panes: Modified (Exit again to discard)", p.fs.failure[0..p.fs.failure_len]);
+ try std.testing.expect(std.mem.indexOf(u8, file.msg[0..file.msg_len], "test.txt") != null);
+ const scratch_pane = p.panes[p.active].?;
+ var named: [64]u8 = undefined;
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], try std.fmt.bufPrint(&named, "{s} (pane {d})", .{ config.scratch_buffer, scratch_pane.serial })) != null);
+ // Delcol, Restore and Exit name it the same way.
+ p.fs.failure_len = 0;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], "(Delcol again") != null);
+ try std.testing.expect(std.mem.indexOf(u8, scratch_pane.msg[0..scratch_pane.msg_len], named[0 .. config.scratch_buffer.len + 7]) != null);
}
test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" {
@@ -1786,6 +1925,7 @@ test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and clo
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
const serial = pane.serial;
try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
try std.testing.expect(p.paneBySerial(serial) != null);
@@ -1797,3 +1937,14 @@ test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and clo
try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
try std.testing.expect(p.paneBySerial(serial) == null);
}
+
+test "a pane renamed but never edited has no text to lose: Del closes it at once" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ p.newScratchBelow(p.active);
+ const pane = try p.setTestFile("loaded\n");
+ pane.file.?.saved_revision = pane.file.?.revision -% 1; // dirty by the rename alone
+ const serial = pane.serial;
+ try std.testing.expect(p.executeBuiltinLine(0, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) == null);
+}