summaryrefslogtreecommitdiff
path: root/src/File.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/File.zig')
-rw-r--r--src/File.zig46
1 files changed, 36 insertions, 10 deletions
diff --git a/src/File.zig b/src/File.zig
index e8a9e2f8..43b921e3 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -57,6 +57,9 @@ pub const State = struct {
/// replaces it: an edit that brings the text back to it is clean again,
/// as an undo to the save point is in acme.
saved_hash: ?u64 = null,
+ /// The length of the text `saved_hash` is of, when it is of one: an
+ /// edit to another length cannot be back to it, and is not hashed.
+ saved_len: ?usize = null,
watch_after_save: bool = false,
/// Save makes the file's directory, and any above it, first: `Config`'s
/// pane for an init file that is not there yet, whose directory may not
@@ -617,14 +620,22 @@ pub fn lineIndex(gpa: std.mem.Allocator, f: *State) ![]const usize {
/// The line index of `new` from `old`'s: an edit replaces one span, so
/// line starts before it stand, starts after it shift, and only the span
/// is scanned. Rebuilding scanned the whole file on every keystroke.
-fn editedLineStarts(gpa: std.mem.Allocator, starts: []const usize, old: []const u8, new: []const u8) ![]usize {
+/// The bytes a splice left alone at either end, when its caller knows them.
+pub const Span = struct { head: usize, tail: usize };
+
+fn editedLineStarts(gpa: std.mem.Allocator, starts: []const usize, old: []const u8, new: []const u8, known: ?Span) ![]usize {
const n = @min(old.len, new.len);
var head: usize = 0;
- while (head + 64 <= n and std.mem.eql(u8, old[head..][0..64], new[head..][0..64])) head += 64;
- while (head < n and old[head] == new[head]) head += 1;
var tail: usize = 0;
- while (tail + 64 <= n - head and std.mem.eql(u8, old[old.len - tail - 64 ..][0..64], new[new.len - tail - 64 ..][0..64])) tail += 64;
- while (tail < n - head and old[old.len - tail - 1] == new[new.len - tail - 1]) tail += 1;
+ if (known) |span| {
+ head = span.head;
+ tail = span.tail;
+ } else {
+ while (head + 64 <= n and std.mem.eql(u8, old[head..][0..64], new[head..][0..64])) head += 64;
+ while (head < n and old[head] == new[head]) head += 1;
+ while (tail + 64 <= n - head and std.mem.eql(u8, old[old.len - tail - 64 ..][0..64], new[new.len - tail - 64 ..][0..64])) tail += 64;
+ while (tail < n - head and old[old.len - tail - 1] == new[new.len - tail - 1]) tail += 1;
+ }
// A start s follows the newline at s-1: kept while that newline is in
// the common head, shifted while it is in the common tail.
const kept = std.sort.upperBound(usize, starts, head, struct {
@@ -677,7 +688,12 @@ test "edited line index equals a rebuilt one" {
const old = try gpa.dupe(u8, text.items);
defer gpa.free(old);
try text.replaceRange(gpa, at, del, ins[0..ins_len]);
- const edited = try editedLineStarts(gpa, starts, old, text.items);
+ // As a splice that says what it kept, and as one that does not.
+ const known: Span = .{ .head = at, .tail = old.len - at - del };
+ const told = try editedLineStarts(gpa, starts, old, text.items, known);
+ defer gpa.free(told);
+ const edited = try editedLineStarts(gpa, starts, old, text.items, null);
+ try std.testing.expectEqualSlices(usize, edited, told);
defer gpa.free(edited);
var fresh_state: State = undefined;
fresh_state.content = text.items;
@@ -906,6 +922,12 @@ fn reportEdit(p: *Pardes, f: *State, new: []const u8) void {
}
pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
+ setContentSpan(p, f, new, null);
+}
+
+/// `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 {
locations.freeRows(p.gpa, f.location_rows);
f.location_rows = &.{};
for (p.panes) |slot| if (slot) |pane| {
@@ -919,13 +941,17 @@ pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
reportEdit(p, f, new);
if (f.mini) |*mini| mini.deinit(p.gpa);
f.mini = null;
- const starts: []usize = if (f.line_starts.len > 0) editedLineStarts(p.gpa, f.line_starts, f.content, new) catch &.{} else &.{};
- // ponytail: a whole-text hash per edit; a rolling hash when files grow large.
- if (f.revision == f.saved_revision or f.saved_hash == null) f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ const starts: []usize = if (f.line_starts.len > 0) editedLineStarts(p.gpa, f.line_starts, f.content, new, span) catch &.{} else &.{};
+ // ponytail: a whole-text hash per edit that keeps the saved length; a
+ // rolling hash when that too matters.
+ if (f.revision == f.saved_revision or f.saved_hash == null) {
+ f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
+ }
p.gpa.free(f.content);
f.content = new;
f.revision +%= 1;
- if (f.saved_hash) |h| if (std.hash.Wyhash.hash(0, new) == h) {
+ if (f.saved_hash) |h| if ((f.saved_len orelse new.len) == new.len and std.hash.Wyhash.hash(0, new) == h) {
f.saved_revision = f.revision;
};
f.mtime = pardes.ctlfs.events.now();