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