diff options
Diffstat (limited to 'src/File.zig')
| -rw-r--r-- | src/File.zig | 239 |
1 files changed, 236 insertions, 3 deletions
diff --git a/src/File.zig b/src/File.zig index 3ef1593d..522d4bd6 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; @@ -107,6 +108,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 }; @@ -992,11 +997,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); @@ -1004,6 +1015,7 @@ 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); + 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); p.gpa.destroy(file.history); @@ -1024,7 +1036,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); @@ -1061,6 +1086,214 @@ 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; +} + +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 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; + 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]) |other| { + 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; + } + } + 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 |
