summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 20:50:11 -0300
committerGabriel Schneider <[email protected]>2026-10-01 20:50:11 -0300
commit081e1b9ab9d3bb8afaae8ea040c7767cb6e4f863 (patch)
tree8184d5f206b00c8e5e18445e1626e360b11ed65f /src/ninep
parent4bdeee602d1e642582ce91fdbd0da210797894e9 (diff)
downloadpardes-081e1b9ab9d3bb8afaae8ea040c7767cb6e4f863.tar.gz
pardes-081e1b9ab9d3bb8afaae8ea040c7767cb6e4f863.zip
Undo after Get file or Edit's e puts the old name back with the old text and its clean state, so a Save or Putall after it writes nothing over the loaded file; a Get file that fails, or is asked about unsaved edits, renames nothing; Get file in a directory pane is refused with how to open it
ctl.getFile is the one load-under-a-name path: it reads first, then makes one undo step (whose snapshot now keeps the name and its saved state), renames and loads, and leaves the pane clean. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig105
1 files changed, 105 insertions, 0 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index ac5d64d8..973f87e2 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1159,6 +1159,38 @@ pub fn getRefused(p: *Pardes, pane: *Pane, word: []const u8) ?[]const u8 {
return std.fmt.bufPrint(&p.fs.ename, "{s}: Modified ({s} again to discard)", .{ f.path, word }) catch "Modified (get again to discard)";
}
+/// acme's `Get file`, and Edit's `e file`: the file at `path` (absolute)
+/// loaded into the pane, which takes its name, in one undo step that puts
+/// the old name back with the old text. Read first, so one that cannot be
+/// read changes nothing; loaded, the pane is clean. Asking about unsaved
+/// edits is the caller's (getRefused). Returns an errno, 0 when loaded.
+pub fn getFile(p: *Pardes, pane: *Pane, path: []const u8, failed: *anyerror) u16 {
+ const f = pane_files.fileOf(pane) orelse return E.INVAL;
+ const id = p.paneBySerial(pane.serial) orelse return E.NOENT;
+ const bytes = fs.read(p, path) catch |err| {
+ failed.* = err;
+ return switch (err) {
+ error.FileNotFound => E.NOENT,
+ else => E.IO,
+ };
+ };
+ panes.File.pushUndo(p, pane);
+ pane_files.nameBuffer(p, id, path, false) catch |err| {
+ p.gpa.free(bytes);
+ failed.* = err;
+ return E.INVAL;
+ };
+ panes.File.setContent(p, f, bytes);
+ f.saved_revision = f.revision;
+ f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
+ f.disk_newer = null;
+ f.disk_newer_warned = false;
+ f.disk_gone = false;
+ if (fs.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } });
+ return 0;
+}
+
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).
@@ -1890,6 +1922,79 @@ test "Zerox makes a second pane on one buffer: one text, undo and dirty state, i
try testing.expect(p.fs.failures != failures);
}
+test "Undo after a load under another name (Get file, Edit e) puts the old name back with the old text and its clean state; Putall then writes nothing over the loaded file" {
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "other.txt", .data = "other\n" });
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "keep.txt", .data = "keep\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var other_buf: [4200]u8 = undefined;
+ const other = try std.fmt.bufPrint(&other_buf, "{s}/other.txt", .{dir});
+ var keep_buf: [4200]u8 = undefined;
+ const keep = try std.fmt.bufPrint(&keep_buf, "{s}/keep.txt", .{dir});
+ for ([_][]const u8{ "Get keep.txt", "Edit e keep.txt" }) |load| {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const pane = p.panes[0].?;
+ var name: [4300]u8 = undefined;
+ _ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&name, "{s}\n", .{other}));
+ try testing.expect(p.executeBuiltinLine(0, "Get"));
+ try testing.expectEqualStrings("other\n", pane.file.?.content);
+ try testing.expect(!pane_files.dirtyOf(pane));
+ // Loaded: keep.txt's text under its name.
+ try testing.expect(p.executeBuiltinLine(0, load));
+ try testing.expectEqualStrings("keep\n", pane.file.?.content);
+ try testing.expectEqualStrings(keep, pane.file.?.path);
+ if (std.mem.startsWith(u8, load, "Get")) try testing.expect(!pane_files.dirtyOf(pane));
+ // Undo: other.txt's text, other.txt's name, clean as it was.
+ try testing.expect(p.executeBuiltinLine(0, "Undo"));
+ try testing.expectEqualStrings("other\n", pane.file.?.content);
+ try testing.expectEqualStrings(other, pane.file.?.path);
+ try testing.expect(!pane_files.dirtyOf(pane));
+ // Putall saves nothing, and nothing lands on keep.txt.
+ while (p.nextEffect()) |_| {}
+ try testing.expect(p.executeBuiltinLine(0, "Putall"));
+ while (p.nextEffect()) |e| try testing.expect(e != .save_file);
+ // Redo: keep.txt again.
+ try testing.expect(p.executeBuiltinLine(0, "Redo"));
+ try testing.expectEqualStrings(keep, pane.file.?.path);
+ try testing.expectEqualStrings("keep\n", pane.file.?.content);
+ }
+}
+
+test "a Get file that fails changes nothing: no such file, a directory, or unsaved edits asked about once" {
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "here.txt", .data = "here\n" });
+ try tmp.dir.createDirPath(testing.io, "sub");
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const pane = p.panes[0].?;
+ var name: [4300]u8 = undefined;
+ const here = try std.fmt.bufPrint(&name, "{s}/here.txt\n", .{dir});
+ _ = wr(p, Node.of(serial, .name), here);
+ try testing.expect(p.executeBuiltinLine(0, "Get"));
+ const undo_len = pane.file.?.history.undo_len;
+ for ([_][]const u8{ "Get nosuch.txt", "Get sub" }) |line| {
+ const failures = p.fs.failures;
+ try testing.expect(p.executeBuiltinLine(0, line));
+ try testing.expect(p.fs.failures != failures);
+ try testing.expectEqualStrings(here[0 .. here.len - 1], pane.file.?.path);
+ try testing.expectEqualStrings("here\n", pane.file.?.content);
+ try testing.expectEqual(undo_len, pane.file.?.history.undo_len);
+ }
+ // Edited, the first Get asks and renames nothing.
+ _ = wr(p, Node.of(serial, .body), "more\n");
+ try testing.expect(p.executeBuiltinLine(0, "Get other.txt"));
+ try testing.expectEqualStrings(here[0 .. here.len - 1], pane.file.?.path);
+ try testing.expectEqualStrings("here\nmore\n", pane.file.?.content);
+}
+
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();