diff options
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 94 |
1 files changed, 75 insertions, 19 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 3f4a680c..9025da6d 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -238,7 +238,7 @@ test "Exit asks once about unsaved text, and quits when asked again" { 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)")); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), ": Modified\n\nExit again to discard\n")); // Told once, and nothing edited since: the second Exit quits. try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); try std.testing.expect(p.quit); @@ -267,6 +267,14 @@ test "Exit over a ctl write or a click names every pane it asks about, a second 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); + // The whole list stays in +Unsaved; the notice is short. + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), first.file.?.path) != null); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), "\nExit again to discard\n")); + var noticed = false; + for (p.panes) |slot| if (slot) |pane| { + noticed = noticed or std.mem.eql(u8, pane.msg[0..pane.msg_len], "2 unsaved panes \u{2014} Exit again to discard"); + }; + try std.testing.expect(noticed); // 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)); @@ -274,6 +282,13 @@ test "Exit over a ctl write or a click names every pane it asks about, a second 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); + // The one +Unsaved, filled again. + var kept: usize = 0; + for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output) |o| { + kept += @intFromBool(o.from == .unsaved); + }; + try std.testing.expectEqual(@as(usize, 1), kept); + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), 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")); @@ -281,6 +296,12 @@ test "Exit over a ctl write or a click names every pane it asks about, a second } } +/// What +Unsaved holds: the list a refusal over unsaved panes keeps. +fn unsavedList(p: *Pardes) []const u8 { + for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output) |o| if (o.from == .unsaved) return f.content; + return ""; +} + /// A dump file that exists, for a Restore to get past reading it. fn testDump(buf: []u8) ![]const u8 { var tmp = std.testing.tmpDir(.{}); @@ -304,7 +325,7 @@ test "Restore asks about unsaved text as Exit does, and restores when asked agai const line = try testDump(&line_buf); try std.testing.expect(p.executeBuiltinLine(p.active, line)); try std.testing.expect(p.restore_req == null); - try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Restore again to discard)")); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), ": Modified\n\nRestore again to discard\n")); try std.testing.expect(p.executeBuiltinLine(p.active, line)); try std.testing.expectEqualStrings(line["Restore ".len..], p.restore_req.?); } @@ -336,7 +357,7 @@ test "a Restore's warning is not an Exit's: each word is warned on its own" { // The Restore warned; an Exit after it has not been, and asks. 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)")); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), ": Modified\n\nExit again to discard\n")); try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); try std.testing.expect(p.quit); } @@ -460,7 +481,9 @@ pub const Exec = struct { /// acme's rowclean (editors/acme/wind.c:511-529, winclean), asked before /// what would throw unsaved text away: each pane holding some nobody has -/// been told about says so, `<name>: Modified (Exit again to discard)`, +/// been told about is listed in +Unsaved, `<name>: Modified`, under it +/// `Exit again to discard`, with a short notice, `3 unsaved panes -- Exit +/// again to discard`, /// naming the word asking, since repeating it throws the text away, and is /// then clean for the asking, as winclean clears `dirty`; so the same word /// again, with nothing edited since, goes ahead. Only the same word: a @@ -482,6 +505,9 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { var count: usize = 0; var one_buf: [limits.host_path_cap + 24]u8 = undefined; var one: []const u8 = ""; + // The whole list goes into +Unsaved, where it stays; the notice is short. + var listed: std.ArrayList(u8) = .empty; + const arena = c.p.scratch.allocator(); for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; switch (which) { @@ -508,12 +534,17 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { // 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); + listed.print(arena, "{s}: Modified\n", .{name}) catch {}; if (count == 0) one = std.fmt.bufPrint(&one_buf, "{s}", .{name}) catch name; count += 1; } + if (count > 0) { + listed.print(arena, "\n{s} again to discard\n", .{@tagName(asking)}) catch {}; + if (c.p.gpa.dupe(u8, listed.items)) |content| panes.Output.openUnsaved(c.p, c.id, content) else |_| {} + var short: [96]u8 = undefined; + c.p.reportFailure(c.id, std.fmt.bufPrint(&short, "{d} unsaved pane{s} \u{2014} {s} again to discard", .{ count, if (count == 1) "" else "s", @tagName(asking) }) catch "unsaved panes"); + } 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) @@ -595,7 +626,8 @@ pub const Kill = struct { pub const Dump = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { - dump.dumpState(c.p) catch {}; + // Never a Dump said done that wrote nothing: its failure is said. + dump.dumpState(c.p) catch |err| c.p.reportError(c.id, "Dump", err); } }; @@ -705,7 +737,14 @@ pub const Unmount = struct { var args = std.mem.tokenizeAny(u8, c.arg orelse "", " \t"); const name = args.next() orelse return c.p.reportError(c.id, "Unmount name", error.MissingArgument); if (args.next() != null) return c.p.reportError(c.id, "Unmount name", error.TooManyArguments); - pardes.filesystem.unmount(c.p, name) catch |err| return c.p.reportError(c.id, "Unmount", err); + pardes.filesystem.unmount(c.p, name) catch |err| { + // "no such": the words 9ns reads as ENOENT. + if (err == error.NotMounted) { + var said: [96]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Unmount: no such mount {s}", .{name[0..@min(name.len, 32)]}) catch "Unmount: no such mount"); + } + return c.p.reportError(c.id, "Unmount", err); + }; } }; @@ -733,7 +772,7 @@ pub const NextColor = struct { }; /// List the themes in a pane, each a word to click. -pub const ThemeSel = struct { +pub const Themes = struct { pub const scope: Scope = .session; pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .commands = true }; pub fn run(c: Ctx) void { @@ -771,7 +810,7 @@ pub const DumpThemes = struct { }; /// List the fonts in a pane, each a word to click. -pub const FontSel = struct { +pub const Fonts = struct { pub const scope: Scope = .session; pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true }; pub const enabled = capabilities.font_picker; @@ -1315,7 +1354,12 @@ pub const Mini = struct { // ---- search ---- -/// List every place a pattern matches in the pane, in a +Search. +/// A Find or Grep that finds nothing fails as one, in words 9ns reads as +/// ENOENT (`not found`), the same for both. +const e_search_miss_find = "find: no match, not found"; +const e_search_miss_grep = "grep: no match, not found"; + +/// List the files below this pane's directory whose names hold its argument, in a +Search. pub const Find = struct { pub const takes_arg = true; pub const requires_arg = true; @@ -1323,7 +1367,8 @@ pub const Find = struct { pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) { - _ = look.runSearch(c.p, c.id, pat, .find, .top, true) catch |err| c.p.reportError(c.id, "find", err); + const rows = look.runSearch(c.p, c.id, pat, .find, .top, false) catch |err| return c.p.reportError(c.id, "find", err); + if (rows == 0) c.p.reportFailure(c.id, e_search_miss_find); return; } c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.find_marker }); @@ -1339,7 +1384,8 @@ pub const Grep = struct { pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) { - _ = look.runSearch(c.p, c.id, pat, .grep, .top, true) catch |err| c.p.reportError(c.id, "grep", err); + const rows = look.runSearch(c.p, c.id, pat, .grep, .top, false) catch |err| return c.p.reportError(c.id, "grep", err); + if (rows == 0) c.p.reportFailure(c.id, e_search_miss_grep); return; } c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.grep_marker }); @@ -1432,6 +1478,7 @@ pub const Last = struct { while (i > 0) { i -= 1; const j = c.p.jumps[i]; + if (j.serial == 0) continue; // a closed file's: Back reopens it, Last goes to a pane // Restore the cursor without recentering the pane's retained view. if (j.pane != c.id) return look.focusPaneLine(c.p, j.pane, .{ .line = j.line, .col = j.col }, .keep); } @@ -1441,6 +1488,15 @@ pub const Last = struct { } }; +/// List the files opened lately, closed ones too, each a place to open again. +pub const Recent = struct { + pub const scope: Scope = .session; + pub const output: OutputTraits = .{ .name = config.recent_buffer, .steps = true, .locations = true }; + pub fn run(c: Ctx) void { + panes.Output.openRecent(c.p, c.id) catch |err| c.p.reportError(c.id, "recent", err); + } +}; + /// List the places jumped from, each a place to go. pub const Jumplist = struct { pub const scope: Scope = .session; @@ -1909,15 +1965,15 @@ test "Exit and Restore name a modified +New scratch as well as files" { try std.testing.expect(!p.quit); // 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); + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), "test.txt: Modified") != 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); + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), 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); + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), "Delcol again to discard") != null); + try std.testing.expect(std.mem.indexOf(u8, unsavedList(p), 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" { @@ -1929,11 +1985,11 @@ test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and clo const serial = pane.serial; try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol")); try std.testing.expect(p.paneBySerial(serial) != null); - try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Delcol again to discard)")); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), ": Modified\n\nDelcol again to discard\n")); // Each word is warned on its own: Del asks too, then closes. try std.testing.expect(p.executeBuiltinLine(p.active, "Del")); try std.testing.expect(p.paneBySerial(serial) != null); - try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Del again to discard)")); + try std.testing.expect(std.mem.endsWith(u8, unsavedList(p), ": Modified\n\nDel again to discard\n")); try std.testing.expect(p.executeBuiltinLine(p.active, "Del")); try std.testing.expect(p.paneBySerial(serial) == null); } |
