diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 17:48:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 18:08:12 -0300 |
| commit | 8c74f9a8f199ea0c2d20cc524358d224ea192005 (patch) | |
| tree | d2a280eb296ad1dce02c9f8e3e8fad20a927edf8 | |
| parent | afec162464d360cbbf475d3fb7457cda83ce918f (diff) | |
| download | pardes-8c74f9a8f199ea0c2d20cc524358d224ea192005.tar.gz pardes-8c74f9a8f199ea0c2d20cc524358d224ea192005.zip | |
Get reads a pane's file again, refusing unsaved edits once as Del does, and Get file loads that file into the pane; in a directory pane it reads the directory again. Load is Restore, as Put is Save; Abort's refusal says it is acme's debugging word
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/typ/builtins-gui.json | 1 | ||||
| -rw-r--r-- | docs/typ/builtins-tty.json | 1 | ||||
| -rw-r--r-- | src/Dir.zig | 4 | ||||
| -rw-r--r-- | src/builtins.zig | 28 | ||||
| -rw-r--r-- | src/config.zig | 1 | ||||
| -rw-r--r-- | src/exec.zig | 11 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 48 |
7 files changed, 74 insertions, 20 deletions
diff --git a/docs/typ/builtins-gui.json b/docs/typ/builtins-gui.json index 53142c1e..7bace0b2 100644 --- a/docs/typ/builtins-gui.json +++ b/docs/typ/builtins-gui.json @@ -42,6 +42,7 @@ {"name": "Font", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "The body font, by name or path, and its size."}, {"name": "Fonts", "leader": "SPC t f", "also": [], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "List the fonts in a pane, each a word to click."}, {"name": "Forward", "leader": "SPC j i", "also": ["C-i"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Ctrl-i: one step forward again, up to wherever Back started."}, + {"name": "Get", "leader": null, "also": [], "arg": true, "scope": "pane", "setting": false, "choices": null, "doc": "acme's Get: the pane's file read again, its unsaved edits refused once as Del refuses them."}, {"name": "Grain", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off, 0-3", "doc": "The page takes a fine, still grain, like paper: a level, 0 (off) through 3."}, {"name": "Grep", "leader": "SPC f g", "also": ["topbar"], "arg": true, "scope": "pane", "setting": false, "choices": null, "doc": "Find's sibling: Find matches file NAMES under this pane's directory, Grep matches file CONTENTS under every pane's directory at once."}, {"name": "GripWidth", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "The grip and scrollbar width, as a percent of the theme's rail."}, diff --git a/docs/typ/builtins-tty.json b/docs/typ/builtins-tty.json index d3dac97c..0e3e6c81 100644 --- a/docs/typ/builtins-tty.json +++ b/docs/typ/builtins-tty.json @@ -37,6 +37,7 @@ {"name": "Find", "leader": "SPC f f", "also": ["topbar"], "arg": true, "scope": "pane", "setting": false, "choices": null, "doc": "List the files below this pane's directory whose names hold its argument, in a +Search."}, {"name": "FocusTint", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "The focused pane's and column's tags are tinted, on or off."}, {"name": "Forward", "leader": "SPC j i", "also": ["C-i"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Ctrl-i: one step forward again, up to wherever Back started."}, + {"name": "Get", "leader": null, "also": [], "arg": true, "scope": "pane", "setting": false, "choices": null, "doc": "acme's Get: the pane's file read again, its unsaved edits refused once as Del refuses them."}, {"name": "Grep", "leader": "SPC f g", "also": ["topbar"], "arg": true, "scope": "pane", "setting": false, "choices": null, "doc": "Find's sibling: Find matches file NAMES under this pane's directory, Grep matches file CONTENTS under every pane's directory at once."}, {"name": "Help", "leader": "SPC ?", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Open the help: every key and every builtin."}, {"name": "Hover", "leader": "SPC l k", "also": [], "arg": false, "scope": "pane", "setting": false, "choices": null, "doc": "Ask the language server about the symbol under the cursor."}, diff --git a/src/Dir.zig b/src/Dir.zig index 979cd51a..36895bda 100644 --- a/src/Dir.zig +++ b/src/Dir.zig @@ -408,10 +408,6 @@ test "a look at an entry is a look from the directory, .. goes up to the open pa try tmp.dir.writeFile(testing.io, .{ .sub_path = "sub/later.txt", .data = "" }); try testing.expect(p.executeBuiltinLine(sub, "Get")); try testing.expectEqualStrings("inner.txt later.txt\n", p.panes[sub].?.file.?.content); - // Get anywhere else is still the acme word pardes has not. - const failures = p.fs.failures; - try testing.expect(p.executeBuiltinLine(0, "Get")); - try testing.expect(p.fs.failures != failures); } test "an exec in a directory pane runs in its directory, and its +Errors goes there" { diff --git a/src/builtins.zig b/src/builtins.zig index 1172fa78..1d524514 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1116,6 +1116,34 @@ 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"); + const f = if (c.pane.file) |*file| file else return c.p.reportFailure(c.id, "Get: only a file pane takes it"); + // A directory pane: the directory read again (Dir.zig), as acme's + // Get is in a directory window. + if (f.listing != null) 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"); + if (ctl.getRefused(c.p, c.pane, "Get")) |said| return c.p.reportFailure(c.id, said); + const typed = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + if (typed.len > 0) { + var home_buf: [4096]u8 = undefined; + const full = std.fs.path.resolvePosix(c.p.scratch.allocator(), &.{ Pardes.paneDir(c.pane), @import("fs.zig").expandHome(typed, &home_buf) }) catch return; + @import("ninep/pane.zig").nameBuffer(c.p, c.id, full, false) catch return c.p.reportFailure(c.id, "Get: invalid file name"); + } + var failed: anyerror = error.Unexpected; + if (ctl.get(c.p, c.pane, &failed) != 0) { + var said: [320]u8 = undefined; + const path = c.pane.file.?.path; + c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Get: {s}: {s}", .{ path[0..@min(path.len, 256)], if (failed == error.FileNotFound) "no such file" else "cannot be read" }) catch "Get: cannot be read"); + } + } +}; + /// Write the pane's text to its file, or to the file its argument names. pub const Save = struct { pub const takes_arg = true; diff --git a/src/config.zig b/src/config.zig index d80e2a02..673e91ed 100644 --- a/src/config.zig +++ b/src/config.zig @@ -58,6 +58,7 @@ pub const leader_path = paths: { .Repl = null, .Kill = null, .Exit = "q", + .Get = null, .ClipYank = "y", .ClipYankMain = "Y", .ClipPaste = "p", diff --git a/src/exec.zig b/src/exec.zig index 92fb6244..30a1a894 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -943,21 +943,20 @@ pub fn namesBuiltin(txt: []const u8) bool { pub const acme_words = [_]struct { acme: []const u8, pardes: ?[]const u8, hint: []const u8 = "" }{ .{ .acme = "Put", .pardes = "Save" }, .{ .acme = "Delete", .pardes = "Del" }, - .{ .acme = "Get", .pardes = null, .hint = ": write get to the pane's ctl" }, .{ .acme = "Putall", .pardes = null, .hint = ": Save each pane" }, .{ .acme = "Snarf", .pardes = null, .hint = ": y yanks the selection" }, .{ .acme = "Cut", .pardes = null, .hint = ": d deletes the selection into a register" }, .{ .acme = "Paste", .pardes = null, .hint = ": p puts a register" }, .{ .acme = "Zerox", .pardes = null }, .{ .acme = "Sort", .pardes = null }, - .{ .acme = "Load", .pardes = null, .hint = ": Restore loads a dump" }, + .{ .acme = "Load", .pardes = "Restore" }, .{ .acme = "ID", .pardes = null }, .{ .acme = "Send", .pardes = null }, .{ .acme = "Tab", .pardes = null }, .{ .acme = "Indent", .pardes = null }, .{ .acme = "Local", .pardes = null }, .{ .acme = "Incl", .pardes = null }, - .{ .acme = "Abort", .pardes = null }, + .{ .acme = "Abort", .pardes = null, .hint = ": a debugging word that crashes acme" }, }; pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool { @@ -968,12 +967,6 @@ pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool { var renamed: [1024 + 16]u8 = undefined; const cmd = for (acme_words) |w| { if (!std.mem.eql(u8, raw[0..word_end], w.acme)) continue; - // Get in a directory pane, its tag or its text: the directory read - // again (Dir.zig), as acme's Get is in a directory window. - if (std.mem.eql(u8, w.acme, "Get")) if (p.panes[id]) |pane| if (pane.file) |f| if (f.listing != null) { - panes.Dir.show(p, id, id); - return true; - }; const now = w.pardes orelse { var said: [128]u8 = undefined; p.reportFailure(id, std.fmt.bufPrint(&said, "invalid: acme's {s} is not a pardes builtin{s}", .{ w.acme, w.hint }) catch "invalid: an acme word pardes has not"); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 0f4e181c..269e9346 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -972,7 +972,7 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { if (std.mem.eql(u8, line, "get")) { if (!apply) continue; // Quoting the line, as a builtin's refusal (Save's) does. - if (getRefused(p, pane)) |said_in_ename| { + if (getRefused(p, pane, "get")) |said_in_ename| { // refuse writes ename, where the words are: copy them out. var said_buf: [320]u8 = undefined; const said = said_buf[0..said_in_ename.len]; @@ -1127,7 +1127,9 @@ const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock"; /// acme's get asks winclean first (exec.c:513): unsaved text is warned /// about once, and the same get again, nothing edited since, discards it. -fn getRefused(p: *Pardes, pane: *Pane) ?[]const u8 { +/// Asked once over unsaved edits, by `word` (ctl's `get`, the Get builtin): +/// the words of the refusal, in ename; null to go ahead. +pub fn getRefused(p: *Pardes, pane: *Pane, word: []const u8) ?[]const u8 { const f = pane_files.fileOf(pane) orelse return null; if (!panes.Output.fileTraits(f.output).saves or f.revision == f.saved_revision) return null; // Dirty by a rename alone is no text to lose: only edits are asked about. @@ -1140,15 +1142,16 @@ fn getRefused(p: *Pardes, pane: *Pane) ?[]const u8 { // write's err record logs it, not a msg too. // And logged `unsaved <serial> <name>`, as the other refusals are. tree.events.noteLog(p, .unsaved, pane); - const listed = std.fmt.allocPrint(p.gpa, "{s}: Modified\n\nget again to discard\n", .{f.path}) catch null; + const listed = std.fmt.allocPrint(p.gpa, "{s}: Modified\n\n{s} again to discard\n", .{ f.path, word }) catch null; if (listed) |content| panes.Output.openUnsaved(p, id, content); + var notice: [64]u8 = undefined; p.fs.unlogged = true; - p.setMessage(id, "1 unsaved pane \u{2014} get again to discard"); + p.setMessage(id, std.fmt.bufPrint(¬ice, "1 unsaved pane \u{2014} {s} again to discard", .{word}) catch "1 unsaved pane"); p.fs.unlogged = false; - return std.fmt.bufPrint(&p.fs.ename, "{s}: Modified (get again to discard)", .{f.path}) catch "Modified (get again to discard)"; + return std.fmt.bufPrint(&p.fs.ename, "{s}: Modified ({s} again to discard)", .{ f.path, word }) catch "Modified (get again to discard)"; } -fn get(p: *Pardes, pane: *Pane, failed: *anyerror) u16 { +pub fn get(p: *Pardes, pane: *Pane, failed: *anyerror) u16 { const f = pane_files.fileOf(pane) orelse return 0; // A directory pane: the directory read again (Dir.zig). if (f.listing != null) { @@ -1775,6 +1778,37 @@ test "get reloads the pane from disk and reports a missing file without losing d try testing.expect(pane_files.dirtyOf(pane)); } +test "Get reads the pane's file again, refusing unsaved edits once, and Get file loads that file into the pane" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "note.txt", .data = "from disk\n" }); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "other.txt", .data = "the other\n" }); + var directory_buf: [4096]u8 = undefined; + const directory = directory_buf[0..try tmp.dir.realPath(testing.io, &directory_buf)]; + const p = try withFile(testing.allocator, "in memory\n"); + defer p.deinit(); + const serial = serialOf(p); + const pane = p.panes[0].?; + var name: [std.fs.max_path_bytes + 8]u8 = undefined; + _ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&name, "{s}/note.txt\n", .{directory})); + try testing.expect(p.executeBuiltinLine(0, "Get")); + try testing.expectEqualStrings("from disk\n", pane.file.?.content); + try testing.expect(!pane_files.dirtyOf(pane)); + // Edited: asked once, then read again. + _ = wr(p, Node.of(serial, .body), "unsaved\n"); + const failures = p.fs.failures; + try testing.expect(p.executeBuiltinLine(0, "Get")); + try testing.expect(p.fs.failures != failures); + try testing.expectEqualStrings("from disk\nunsaved\n", pane.file.?.content); + try testing.expect(p.executeBuiltinLine(0, "Get")); + try testing.expectEqualStrings("from disk\n", pane.file.?.content); + // Get file: that file, by a name from the pane's directory. + try testing.expect(p.executeBuiltinLine(0, "Get other.txt")); + try testing.expectEqualStrings("the other\n", pane.file.?.content); + try testing.expect(std.mem.endsWith(u8, pane.file.?.path, "/other.txt")); + try testing.expect(!pane_files.dirtyOf(pane)); +} + test "a body write to a terminal is typed input, not a paste, even to a program that asked for bracketed paste" { const p = try Pardes.init(testing.allocator, .{ .cols = 90, .rows = 30 }); defer p.deinit(); @@ -3157,7 +3191,7 @@ test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask) const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); const serial = serialOf(p); - for ([_][]const u8{ "Snarf\n", "Putall\n", "Zerox\n", "Get\n", "Tab\n", "Indent\n", "Local\n", "Incl\n", "Abort\n" }) |line| { + for ([_][]const u8{ "Snarf\n", "Putall\n", "Zerox\n", "Tab\n", "Indent\n", "Local\n", "Incl\n", "Abort\n" }) |line| { for ([_]u64{ Node.of(serial, .ctl), Node.of(serial, .exec) }) |node| { const r = wr(p, node, line); try testing.expectEqual(E.INVAL, r.errno()); |
