summaryrefslogtreecommitdiff
path: root/src/File.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/File.zig')
-rw-r--r--src/File.zig419
1 files changed, 389 insertions, 30 deletions
diff --git a/src/File.zig b/src/File.zig
index b3c28290..875d0f46 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -3,6 +3,7 @@
const panes = @import("panes.zig");
const std = @import("std");
const vaxis = @import("vaxis");
+const builtin = @import("builtin");
const pardes = @import("pardes.zig");
const config = @import("config.zig");
const Pardes = pardes.Pardes;
@@ -31,20 +32,101 @@ pub const Snapshot = struct {
cur_row: i32,
cur_col: i32,
vsel: Text.CharSel,
+ /// The pane's name then, as History.gen counts its renames, and whether
+ /// and as what the text 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. No allocation a step: a name is kept once, at its rename.
+ gen: u32 = 0,
+ clean: bool = false,
+ saved_hash: ?u64 = null,
+ saved_len: ?usize = null,
+
+ fn deinit(s: Snapshot, gpa: std.mem.Allocator) void {
+ gpa.free(s.content);
+ }
};
+/// 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 {
+ return .{
+ .content = gpa.dupe(u8, f.content) catch return null,
+ .cur_row = pane.body.cur_row,
+ .cur_col = pane.body.cur_col,
+ .vsel = pane.body.vsel,
+ .gen = f.history.gen,
+ .clean = f.revision == f.saved_revision,
+ .saved_hash = f.saved_hash,
+ .saved_len = f.saved_len,
+ };
+}
+
+/// A rename (ninep/pane.zig nameBuffer): the name it ends, kept for an undo
+/// back across it; `old` is the History's from here (owned), freed if it
+/// cannot be kept.
+pub fn noteRenamed(gpa: std.mem.Allocator, history: *History, old: []u8) void {
+ history.names.append(gpa, .{ .gen = history.gen, .path = old }) catch gpa.free(old);
+ history.gen +%= 1;
+}
+
+/// 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 {
+ const history = f.history;
+ if (snap.gen == history.gen) return;
+ // The latest name kept for that generation.
+ var at = history.names.items.len;
+ const path = while (at > 0) {
+ at -= 1;
+ if (history.names.items[at].gen == snap.gen) break history.names.items[at].path;
+ } else return;
+ if (std.mem.eql(u8, path, f.path)) {
+ history.gen = snap.gen;
+ return;
+ }
+ const id = for (p.panes, 0..) |slot, i| {
+ if (slot == pane) break i;
+ } else return;
+ // nameBuffer frees nothing of `path`: it keeps the name it ends.
+ const back = p.gpa.dupe(u8, path) catch return;
+ defer p.gpa.free(back);
+ pardes.ctlfs.pane.nameBuffer(p, id, back, false) catch return;
+ history.gen = snap.gen;
+ 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,
redo: [limits.undo_max]Snapshot = undefined,
redo_len: usize = 0,
+ /// The names the file had, each with the rename generation it ended
+ /// (Snapshot.gen), and the generation now.
+ names: std.ArrayList(struct { gen: u32, path: []u8 }) = .empty,
+ gen: u32 = 0,
+
pub fn create(gpa: std.mem.Allocator) !*History {
const history = try gpa.create(History);
history.undo_len = 0;
history.redo_len = 0;
+ history.names = .empty;
+ history.gen = 0;
return history;
}
+
+ pub fn destroy(history: *History, gpa: std.mem.Allocator) void {
+ for (history.undo[0..history.undo_len]) |snap| snap.deinit(gpa);
+ for (history.redo[0..history.redo_len]) |snap| snap.deinit(gpa);
+ for (history.names.items) |name| gpa.free(name.path);
+ history.names.deinit(gpa);
+ gpa.destroy(history);
+ }
};
/// A file pane's backing: owned content, its derived caches, and undo history.
@@ -84,6 +166,8 @@ pub const State = struct {
mtime: u32 = 0,
/// Non-null for a generated output buffer rather than an on-disk file.
output: ?Output.State = null,
+ /// A directory pane's entries (Dir.zig), its output `.dir`.
+ listing: ?panes.Dir.State = null,
mini: ?Mini.State = null,
scroll: usize = 0,
/// `line_starts[i]` is line i's byte offset. Empty means not built yet.
@@ -105,6 +189,10 @@ pub const State = struct {
/// to; gpa-owned ranges and the primary's index.
node_history: std.ArrayList(NodeStep) = .empty,
history: *History,
+ /// Zerox's group: the panes on one buffer share `content`, `history`
+ /// and (while it is the one index) `line_starts`, each with its own
+ /// scroll, cursor and syntax; 0, none. See `zerox`.
+ twin: u32 = 0,
};
pub const NodeStep = struct { ranges: []modal.Selection, primary: usize };
@@ -309,7 +397,7 @@ test "stacked location metadata requires matching adjacent preview ownership" {
}
pub fn graphemeDisplayWidth(grapheme: []const u8) usize {
- if (std.mem.eql(u8, grapheme, "\t")) return config.tab_width;
+ if (std.mem.eql(u8, grapheme, "\t")) return config.tab_now;
if (grapheme.len == 1 and grapheme[0] >= 0x20 and grapheme[0] < 0x7f) return 1;
if (std.unicode.utf8ValidateSlice(grapheme)) return @max(1, @as(usize, vaxis.gwidth.gwidth(grapheme, .unicode)));
// vaxis's gwidth counts back a replacement rune's 3 bytes over the one
@@ -344,7 +432,7 @@ test "an invalid byte in a grapheme is one cell, and never reaches vaxis's width
}
pub fn byteDisplayWidth(byte: u8) usize {
- return if (byte == '\t') config.tab_width else 1;
+ return if (byte == '\t') config.tab_now else 1;
}
pub fn displayWidth(text: []const u8) usize {
@@ -501,7 +589,7 @@ pub fn renderedLineDisplayCol(pane: *const Pane, row: i32, line_text: []const u8
test "the ASCII arm of graphemeDisplayWidth matches the gwidth it skips" {
const ref = struct {
fn width(grapheme: []const u8) usize {
- if (std.mem.eql(u8, grapheme, "\t")) return config.tab_width;
+ if (std.mem.eql(u8, grapheme, "\t")) return config.tab_now;
return @max(1, @as(usize, vaxis.gwidth.gwidth(grapheme, .unicode)));
}
}.width;
@@ -943,6 +1031,14 @@ pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane {
};
pane.cols = @max(1, src.cols);
pane.rows = @max(1, src.rows);
+ // A directory pane is its directory as it is now, laid out at the next
+ // sync; one that cannot be read keeps the text the dump kept.
+ if (output) |o| if (o.from == .dir) {
+ const f = &pane.file.?;
+ if (panes.Dir.read(p.gpa, panes.Dir.ofName(path))) |names| {
+ f.listing = .{ .names = names, .revision = f.revision };
+ } else |_| f.listing = .{ .names = &.{}, .revision = f.revision -% 1 };
+ };
if (output == null and filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true, .mode = .baseline_disk } });
// Its file gone from disk since the dump: its text is nowhere else now,
// so it comes back as one deleted on disk, dirty, and Del, Exit and
@@ -982,10 +1078,17 @@ pub fn deinit(p: *Pardes, pane: *Pane, file: *State) void {
if (file.output == null) for (p.panes, 0..) |slot, id| {
if (slot == pane) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } });
};
+ // A twin's text, undo and shared index stay with the twins left.
+ var twins: [pardes.MAX_PANES]usize = undefined;
+ const ntwins = twinsOf(p, file, &twins);
+ const index_shared = for (twins[0..ntwins]) |id| {
+ if (p.panes[id].?.file.?.line_starts.ptr == file.line_starts.ptr) break true;
+ } else false;
p.gpa.free(file.path);
- p.gpa.free(file.content);
+ if (ntwins == 0) p.gpa.free(file.content);
+ if (file.listing) |listing| p.gpa.free(listing.names);
if (file.mini) |*mini| mini.deinit(p.gpa);
- if (file.line_starts.len > 0) p.gpa.free(file.line_starts);
+ if (file.line_starts.len > 0 and !index_shared) p.gpa.free(file.line_starts);
if (file.highlights.len > 0) p.tree_sitter_gpa.free(file.highlights);
p.gpa.free(file.pager_spans);
locations.freeRows(p.gpa, file.location_rows);
@@ -993,9 +1096,8 @@ pub fn deinit(p: *Pardes, pane: *Pane, file: *State) void {
if (file.node_tree) |tree| syntax.freeTree(tree);
for (file.node_history.items) |step| p.gpa.free(step.ranges);
file.node_history.deinit(p.gpa);
- 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);
- p.gpa.destroy(file.history);
+ if (ntwins > 0) return;
+ file.history.destroy(p.gpa);
}
fn reportEdit(p: *Pardes, f: *State, new: []const u8) void {
@@ -1013,7 +1115,20 @@ pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
/// `setContent` for a splice that knows what it left alone at either end,
/// so the line index is not found again by comparing the whole text.
-pub fn setContentSpan(p: *Pardes, f: *State, new: []u8, span: ?Span) void {
+pub fn setContentSpan(p: *Pardes, f: *State, new: []u8, span_in: ?Span) void {
+ // Twins (Zerox) take the same splice: where it is, found once here when
+ // the caller did not say, is what the line index wants anyway.
+ var twins: [pardes.MAX_PANES]usize = undefined;
+ const ntwins = twinsOf(p, f, &twins);
+ const span: ?Span = span_in orelse if (ntwins > 0) spanOf(f.content, new) else null;
+ var moved: [pardes.MAX_PANES]struct { cursor: usize, scroll: usize } = undefined;
+ for (twins[0..ntwins], 0..) |id, i| {
+ const t = p.panes[id].?;
+ const tf = &t.file.?;
+ const at = lineStart(p.gpa, tf, @intCast(@max(0, t.body.cur_row))) + @as(usize, @intCast(@max(0, t.body.cur_col)));
+ moved[i] = .{ .cursor = spliced(@min(at, f.content.len), f.content.len, new.len, span.?), .scroll = spliced(lineStart(p.gpa, tf, tf.scroll), f.content.len, new.len, span.?) };
+ }
+ const old_starts = f.line_starts.ptr;
locations.freeRows(p.gpa, f.location_rows);
f.location_rows = &.{};
p.gpa.free(f.pager_spans);
@@ -1050,6 +1165,257 @@ pub fn setContentSpan(p: *Pardes, f: *State, new: []u8, span: ?Span) void {
f.highlight_start = 0;
f.syntax_dirty = true;
if (std.mem.endsWith(u8, f.path, "/+Pager")) f.pager_diff = looksLikeDiff(new[0..@min(new.len, 64 * 1024)]);
+ for (twins[0..ntwins], 0..) |id, i| {
+ const t = p.panes[id].?;
+ const tf = &t.file.?;
+ // Its own index, built while the shared one was empty, goes; the
+ // shared one, freed above, is replaced.
+ if (tf.line_starts.len > 0 and tf.line_starts.ptr != old_starts) p.gpa.free(tf.line_starts);
+ tf.content = f.content;
+ tf.line_starts = f.line_starts;
+ tf.revision = f.revision;
+ tf.saved_revision = f.saved_revision;
+ tf.saved_hash = f.saved_hash;
+ tf.saved_len = f.saved_len;
+ tf.mtime = f.mtime;
+ if (tf.highlights.len > 0) p.tree_sitter_gpa.free(tf.highlights);
+ tf.highlights = &.{};
+ tf.highlight_start = 0;
+ tf.syntax_dirty = true;
+ locations.freeRows(p.gpa, tf.location_rows);
+ tf.location_rows = &.{};
+ if (tf.mini) |*mini| mini.deinit(p.gpa);
+ tf.mini = null;
+ for (0..t.sel.len) |button| {
+ t.clearPointerSelection(button);
+ t.sel[button].state = .none;
+ }
+ t.body.vsel.active = false;
+ t.body.msel.active = false;
+ t.body.nsel = 0;
+ const cursor = positionIn(p.gpa, tf, moved[i].cursor);
+ t.body.cur_row = @intCast(cursor.row);
+ t.body.cur_col = @intCast(cursor.col);
+ tf.scroll = positionIn(p.gpa, tf, moved[i].scroll).row;
+ }
+}
+
+/// Where an offset into the old text is in the new, after the splice that
+/// kept `span` at either end: past it, moved by what it added; inside it,
+/// at its start, as acme moves a clone's q0 (textinsert, textdelete).
+fn spliced(at: usize, old_len: usize, new_len: usize, span: Span) usize {
+ if (at <= span.head) return at;
+ if (at >= old_len - span.tail) return at - (old_len - span.tail) + (new_len - span.tail);
+ return span.head;
+}
+
+/// What `old` and `new` agree on at either end.
+fn spanOf(old: []const u8, new: []const u8) Span {
+ const n = @min(old.len, new.len);
+ const head = std.mem.indexOfDiff(u8, old[0..n], new[0..n]) orelse n;
+ var tail: usize = 0;
+ while (tail < n - head and old[old.len - 1 - tail] == new[new.len - 1 - tail]) tail += 1;
+ return .{ .head = head, .tail = tail };
+}
+
+fn positionIn(gpa: std.mem.Allocator, f: *State, at: usize) struct { row: usize, col: usize } {
+ const index = lineIndex(gpa, f) catch {
+ const c = modal.positionAt(f.content, at);
+ return .{ .row = c.row, .col = c.col };
+ };
+ const row = std.sort.upperBound(usize, index, at, struct {
+ fn order(key: usize, item: usize) std.math.Order {
+ return std.math.order(key, item);
+ }
+ }.order) -| 1;
+ return .{ .row = row, .col = at - index[row] };
+}
+
+/// The other panes on `f`'s buffer (Zerox), by id.
+pub fn twinsOf(p: *Pardes, f: *const State, out: *[pardes.MAX_PANES]usize) usize {
+ if (f.twin == 0) return 0;
+ var n: usize = 0;
+ for (p.panes, 0..) |slot, id| {
+ const pane = slot orelse continue;
+ const other = if (pane.file) |*file| file else continue;
+ if (other == f or other.twin != f.twin) continue;
+ out[n] = id;
+ n += 1;
+ }
+ return n;
+}
+
+/// acme's Zerox: pane `to` made on `from`'s buffer, its text, undo and
+/// dirty state shared, its scroll and cursor its own. An edit in either is
+/// spliced into both (setContentSpan, O(the edit) more); what Save and the
+/// disk watch change on one is copied to the others after each step
+/// (syncTwins). An event reader on one hears no edit made in another.
+pub fn zerox(p: *Pardes, from: usize, to: usize) !*Pane {
+ const src = p.panes[from].?;
+ const f = &src.file.?;
+ const path = try p.gpa.dupe(u8, f.path);
+ errdefer p.gpa.free(path);
+ if (f.twin == 0) {
+ const group = try p.gpa.dupe(u8, f.path);
+ errdefer p.gpa.free(group);
+ try p.twin_groups.append(p.gpa, .{ .id = p.next_twin, .saved_revision = f.saved_revision, .saved_hash = f.saved_hash, .saved_len = f.saved_len, .disk_newer = f.disk_newer, .disk_gone = f.disk_gone, .path = group });
+ f.twin = p.next_twin;
+ p.next_twin +%= 1;
+ }
+ const pane = try p.newDocPane(to);
+ pane.file = .{
+ .path = path,
+ .content = f.content,
+ .line_starts = f.line_starts,
+ .history = f.history,
+ .revision = f.revision,
+ .saved_revision = f.saved_revision,
+ .saved_hash = f.saved_hash,
+ .saved_len = f.saved_len,
+ .disk_newer = f.disk_newer,
+ .disk_gone = f.disk_gone,
+ .mtime = f.mtime,
+ .scroll = f.scroll,
+ .tree_context = f.tree_context,
+ .twin = f.twin,
+ };
+ pane.body.cur_row = src.body.cur_row;
+ pane.body.cur_col = src.body.cur_col;
+ pane.body.cur_pinned = true;
+ if (filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(to), .on = true } });
+ return pane;
+}
+
+/// A twin closed: it keeps a text of its own until it is torn down (a
+/// retired pane is drawn going), and the twins keep the shared one, its
+/// undo and its index.
+pub fn leaveTwins(p: *Pardes, f: *State) void {
+ var twins: [pardes.MAX_PANES]usize = undefined;
+ const n = twinsOf(p, f, &twins);
+ f.twin = 0;
+ if (n == 0) return;
+ const shared_index = for (twins[0..n]) |id| {
+ if (p.panes[id].?.file.?.line_starts.ptr == f.line_starts.ptr) break true;
+ } else false;
+ if (shared_index) f.line_starts = &.{};
+ f.content = p.gpa.dupe(u8, f.content) catch p.gpa.alloc(u8, 0) catch &.{};
+ f.history = History.create(p.gpa) catch f.history;
+ // The one left is no twin now (syncTwins drops the group).
+ if (n == 1) p.panes[twins[0]].?.file.?.twin = 0;
+}
+
+/// Restore's: pane `id`, restored on its own, back on pane `of`'s buffer
+/// as the Zerox twin it was dumped as (dump.File.twin_of). Not a file pane
+/// there (a dump edited by hand), it stays on its own.
+pub fn relink(p: *Pardes, of: usize, id: usize, dot: ?[2]u32) void {
+ const src = p.panes[of] orelse return;
+ const pane = p.panes[id] orelse return;
+ const sf = if (src.file) |*file| file else return;
+ const f = if (pane.file) |*file| file else return;
+ if (sf.output != null or f.output != null or sf.mini != null) return;
+ if (sf.twin == 0) {
+ const group = p.gpa.dupe(u8, sf.path) catch return;
+ p.twin_groups.append(p.gpa, .{ .id = p.next_twin, .saved_revision = sf.saved_revision, .saved_hash = sf.saved_hash, .saved_len = sf.saved_len, .disk_newer = sf.disk_newer, .disk_gone = sf.disk_gone, .path = group }) catch return p.gpa.free(group);
+ sf.twin = p.next_twin;
+ p.next_twin +%= 1;
+ }
+ p.gpa.free(f.content);
+ if (f.line_starts.len > 0) p.gpa.free(f.line_starts);
+ f.history.destroy(p.gpa);
+ f.content = sf.content;
+ f.line_starts = sf.line_starts;
+ f.history = sf.history;
+ f.revision = sf.revision;
+ f.saved_revision = sf.saved_revision;
+ f.saved_hash = sf.saved_hash;
+ f.saved_len = sf.saved_len;
+ f.twin = sf.twin;
+ if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights);
+ f.highlights = &.{};
+ f.syntax_dirty = true;
+ // Its dot as dumped, read now that its text is back.
+ if (dot) |d| if (d[0] <= d[1] and d[1] <= f.content.len) {
+ const a = modal.positionAt(f.content, d[0]);
+ const h = modal.positionAt(f.content, if (d[1] > d[0]) modal.prevGrapheme(f.content, d[1]) else d[0]);
+ pane.body.vsel = .{ .active = d[1] > d[0], .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
+ pane.body.cur_row = @intCast(h.row);
+ pane.body.cur_col = @intCast(h.col);
+ };
+}
+
+pub const TwinGroup = struct {
+ id: u32,
+ saved_revision: u32,
+ saved_hash: ?u64,
+ saved_len: ?usize,
+ disk_newer: ?u64,
+ disk_gone: bool,
+ path: []u8,
+};
+
+/// After each step: what a Save, a get, the disk watch or a rename changed
+/// on one twin is copied to the others; a group down to one pane is no
+/// group. O(panes) a step, and nothing at all with no twins.
+pub fn syncTwins(p: *Pardes) void {
+ var g: usize = 0;
+ while (g < p.twin_groups.items.len) {
+ const group = &p.twin_groups.items[g];
+ var members: [pardes.MAX_PANES]*State = undefined;
+ var member_panes: [pardes.MAX_PANES]*Pane = undefined;
+ var n: usize = 0;
+ for (p.panes) |slot| {
+ const pane = slot orelse continue;
+ const f = if (pane.file) |*file| file else continue;
+ if (f.twin == group.id) {
+ members[n] = f;
+ member_panes[n] = pane;
+ n += 1;
+ }
+ }
+ if (n < 2) {
+ if (n == 1) members[0].twin = 0;
+ p.gpa.free(group.path);
+ _ = p.twin_groups.swapRemove(g);
+ continue;
+ }
+ for (members[0..n]) |f| {
+ if (f.saved_revision == group.saved_revision and f.saved_hash == group.saved_hash and f.saved_len == group.saved_len and
+ f.disk_newer == group.disk_newer and f.disk_gone == group.disk_gone and std.mem.eql(u8, f.path, group.path)) continue;
+ // This one changed: the group, and every other twin, take it.
+ if (!std.mem.eql(u8, f.path, group.path)) {
+ const path = p.gpa.dupe(u8, f.path) catch break;
+ p.gpa.free(group.path);
+ group.path = path;
+ }
+ group.saved_revision = f.saved_revision;
+ group.saved_hash = f.saved_hash;
+ group.saved_len = f.saved_len;
+ group.disk_newer = f.disk_newer;
+ group.disk_gone = f.disk_gone;
+ for (members[0..n], member_panes[0..n]) |other, other_pane| {
+ if (other == f) continue;
+ other.saved_revision = f.saved_revision;
+ other.saved_hash = f.saved_hash;
+ other.saved_len = f.saved_len;
+ other.disk_newer = f.disk_newer;
+ other.disk_gone = f.disk_gone;
+ if (!std.mem.eql(u8, other.path, f.path)) {
+ const path = p.gpa.dupe(u8, f.path) catch continue;
+ p.gpa.free(other.path);
+ other.path = path;
+ // Renamed, each twin says so, as the one renamed did.
+ pardes.ctlfs.events.noteLog(p, .rename, other_pane);
+ }
+ }
+ break;
+ }
+ // Tests hold the twins to one text, one revision, every step.
+ if (builtin.is_test) for (members[1..n]) |other| {
+ std.debug.assert(other.content.ptr == members[0].content.ptr and other.content.len == members[0].content.len);
+ std.debug.assert(other.revision == members[0].revision);
+ };
+ g += 1;
+ }
}
/// Whether `head`, a text's first bytes, holds a diff: a `diff` header, or
@@ -1096,7 +1462,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;
}
@@ -1107,15 +1473,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 last.gen == history.gen) 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;
}
@@ -1123,16 +1487,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);
}
@@ -1140,16 +1500,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);
}
@@ -1182,6 +1538,9 @@ pub fn changed(p: *Pardes, id: u8, bytes: []const u8) void {
if (std.mem.eql(u8, f.content, bytes)) return;
if (f.saved_hash) |saved| if (f.revision != f.saved_revision and std.hash.Wyhash.hash(0, f.content) != saved) {
const disk = std.hash.Wyhash.hash(0, bytes);
+ // The disk holds what the text was read or saved as: no news, only
+ // a watch looking again (an undo back to this name, a restart).
+ if (disk == saved) return;
if (f.disk_newer == disk) return;
f.disk_newer = disk;
f.disk_newer_warned = false;