summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/File.zig86
-rw-r--r--src/builtins.zig20
-rw-r--r--src/ninep/ctl.zig105
3 files changed, 178 insertions, 33 deletions
diff --git a/src/File.zig b/src/File.zig
index 522d4bd6..0f1579b2 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -32,8 +32,58 @@ pub const Snapshot = struct {
cur_row: i32,
cur_col: i32,
vsel: Text.CharSel,
+ /// The name the text had then (owned), and whether and as what it was
+ /// saved under it: an undo across a load into the pane under another
+ /// name (Get file, Edit's e) puts the name back with the text, and the
+ /// dirty state that went with them, as acme's undo does.
+ path: ?[]u8 = null,
+ clean: bool = false,
+ saved_hash: ?u64 = null,
+ saved_len: ?usize = null,
+
+ fn deinit(s: Snapshot, gpa: std.mem.Allocator) void {
+ gpa.free(s.content);
+ if (s.path) |path| gpa.free(path);
+ }
};
+/// The pane's text, place and name now, for an undo or redo step.
+fn snapshotOf(gpa: std.mem.Allocator, pane: *const Pane, f: *const State) ?Snapshot {
+ const content = gpa.dupe(u8, f.content) catch return null;
+ const path = gpa.dupe(u8, f.path) catch {
+ gpa.free(content);
+ return null;
+ };
+ return .{
+ .content = content,
+ .cur_row = pane.body.cur_row,
+ .cur_col = pane.body.cur_col,
+ .vsel = pane.body.vsel,
+ .path = path,
+ .clean = f.revision == f.saved_revision,
+ .saved_hash = f.saved_hash,
+ .saved_len = f.saved_len,
+ };
+}
+
+/// After a step's text went in: if it was under another name, that name
+/// back, and the step's saved state with it (Get file, Edit's e).
+fn restoreName(p: *Pardes, pane: *Pane, f: *State, snap: Snapshot) void {
+ defer if (snap.path) |path| p.gpa.free(path);
+ const path = snap.path orelse return;
+ if (std.mem.eql(u8, path, f.path)) return;
+ const id = for (p.panes, 0..) |slot, i| {
+ if (slot == pane) break i;
+ } else return;
+ pardes.ctlfs.pane.nameBuffer(p, id, path, false) catch return;
+ f.saved_hash = snap.saved_hash;
+ f.saved_len = snap.saved_len;
+ f.saved_revision = if (snap.clean) f.revision else f.revision -% 1;
+ f.disk_newer = null;
+ f.disk_newer_warned = false;
+ if (filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } });
+}
+
pub const History = struct {
undo: [limits.undo_max]Snapshot = undefined,
undo_len: usize = 0,
@@ -1016,8 +1066,8 @@ pub fn deinit(p: *Pardes, pane: *Pane, file: *State) void {
for (file.node_history.items) |step| p.gpa.free(step.ranges);
file.node_history.deinit(p.gpa);
if (ntwins > 0) return;
- for (file.history.undo[0..file.history.undo_len]) |snap| p.gpa.free(snap.content);
- for (file.history.redo[0..file.history.redo_len]) |snap| p.gpa.free(snap.content);
+ for (file.history.undo[0..file.history.undo_len]) |snap| snap.deinit(p.gpa);
+ for (file.history.redo[0..file.history.redo_len]) |snap| snap.deinit(p.gpa);
p.gpa.destroy(file.history);
}
@@ -1340,7 +1390,7 @@ pub fn restoreSnap(pane: *Pane, f: *State, snap: Snapshot) void {
fn pushHistory(gpa: std.mem.Allocator, slots: []Snapshot, len: *usize, snap: Snapshot) void {
if (len.* == slots.len) {
- gpa.free(slots[0].content);
+ slots[0].deinit(gpa);
std.mem.copyForwards(Snapshot, slots[0 .. slots.len - 1], slots[1..]);
len.* -= 1;
}
@@ -1351,15 +1401,13 @@ fn pushHistory(gpa: std.mem.Allocator, slots: []Snapshot, len: *usize, snap: Sna
pub fn pushUndo(p: *Pardes, pane: *Pane) void {
const f = if (pane.file) |*file| file else return;
const history = f.history;
- if (history.undo_len > 0 and std.mem.eql(u8, history.undo[history.undo_len - 1].content, f.content)) return;
- const snap: Snapshot = .{
- .content = p.gpa.dupe(u8, f.content) catch return,
- .cur_row = pane.body.cur_row,
- .cur_col = pane.body.cur_col,
- .vsel = pane.body.vsel,
- };
+ if (history.undo_len > 0) {
+ const last = history.undo[history.undo_len - 1];
+ if (std.mem.eql(u8, last.content, f.content) and std.mem.eql(u8, last.path orelse f.path, f.path)) return;
+ }
+ const snap = snapshotOf(p.gpa, pane, f) orelse return;
pushHistory(p.gpa, &history.undo, &history.undo_len, snap);
- for (history.redo[0..history.redo_len]) |item| p.gpa.free(item.content);
+ for (history.redo[0..history.redo_len]) |item| item.deinit(p.gpa);
history.redo_len = 0;
}
@@ -1367,16 +1415,12 @@ pub fn undo(p: *Pardes, pane: *Pane) void {
const f = if (pane.file) |*file| file else return;
const history = f.history;
if (history.undo_len == 0) return;
- const current: Snapshot = .{
- .content = p.gpa.dupe(u8, f.content) catch return,
- .cur_row = pane.body.cur_row,
- .cur_col = pane.body.cur_col,
- .vsel = pane.body.vsel,
- };
+ const current = snapshotOf(p.gpa, pane, f) orelse return;
pushHistory(p.gpa, &history.redo, &history.redo_len, current);
history.undo_len -= 1;
const previous = history.undo[history.undo_len];
setContent(p, f, previous.content);
+ restoreName(p, pane, f, previous);
restoreSnap(pane, f, previous);
}
@@ -1384,16 +1428,12 @@ pub fn redo(p: *Pardes, pane: *Pane) void {
const f = if (pane.file) |*file| file else return;
const history = f.history;
if (history.redo_len == 0) return;
- const current: Snapshot = .{
- .content = p.gpa.dupe(u8, f.content) catch return,
- .cur_row = pane.body.cur_row,
- .cur_col = pane.body.cur_col,
- .vsel = pane.body.vsel,
- };
+ const current = snapshotOf(p.gpa, pane, f) orelse return;
pushHistory(p.gpa, &history.undo, &history.undo_len, current);
history.redo_len -= 1;
const next = history.redo[history.redo_len];
setContent(p, f, next.content);
+ restoreName(p, pane, f, next);
restoreSnap(pane, f, next);
}
diff --git a/src/builtins.zig b/src/builtins.zig
index ed6ac467..129dd659 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1142,21 +1142,21 @@ pub const Get = struct {
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");
+ const typed = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
// 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);
+ // Get is in a directory window; a file is a Look's from there.
+ if (f.listing != null) {
+ if (typed.len > 0) return c.p.reportFailure(c.id, "Get: a directory pane reads its directory; a Look at the file opens it");
+ 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 home_buf: [4096]u8 = undefined;
+ const path = if (typed.len == 0) c.pane.file.?.path else std.fs.path.resolvePosix(c.p.scratch.allocator(), &.{ Pardes.paneDir(c.pane), @import("fs.zig").expandHome(typed, &home_buf) }) catch return;
+ const errno = if (typed.len == 0) ctl.get(c.p, c.pane, &failed) else ctl.getFile(c.p, c.pane, path, &failed);
+ if (errno != 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");
}
}
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();