diff options
Diffstat (limited to 'src/File.zig')
| -rw-r--r-- | src/File.zig | 419 |
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; |
