summaryrefslogtreecommitdiff
path: root/src/File.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/File.zig')
-rw-r--r--src/File.zig239
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