Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions plugins/text/src/Document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -576,9 +576,7 @@ pub const step_format = 1;
/// Report a step of this document's history to whoever records it (`sdk.Host.recordStep`), when
/// anyone does: nothing is encoded otherwise.
fn recordStep(self: *const Document, step: sdk.Step) void {
const host = sdk.host();
if (!host.recording(self.id)) return;
host.recordStep(self.id, step);
sdk.host().recordStep(self.id, step);
}

/// What `history.end` or `pushComplete` did, reported as a step.
Expand All @@ -589,15 +587,17 @@ pub fn recordCommitted(self: *const Document, c: tc.History.Committed) void {
/// `edits` of history step `id` reported as a step, forward or (`reverse`) as their undo, `sel`
/// the selection they leave.
fn recordEdits(self: *const Document, kind: sdk.Step.Kind, id: u64, edits: []const tc.Transaction.Edit, sel: tc.Range, reverse: bool) void {
if (!sdk.host().recording(self.id)) return;
// Every step reaches the host, which keeps who made it; its bytes only while it is recorded.
const top: u64 = if (self.history.lastApplied()) |r| r.step else 0;
if (!sdk.host().recording(self.id)) return self.recordStep(.{ .kind = kind, .id = id, .top = top });
const gpa = sdk.allocator();
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
tc.Transaction.encodeStep(&out.writer, edits, sel, reverse) catch {
std.log.err("text: a step of {s} was not recorded: out of memory", .{self.path});
return;
};
self.recordStep(.{ .kind = kind, .id = id, .effect = out.written() });
self.recordStep(.{ .kind = kind, .id = id, .effect = out.written(), .top = top });
}

/// Apply a recorded step (`step_format`) as a new step of this document's history: the edits
Expand Down
8 changes: 7 additions & 1 deletion plugins/text/src/TextEditor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,13 @@ fn editNotifyInserted(ctx: *anyopaque, pos: usize, bytes: []const u8) void {
}
fn editNotifyEnd(ctx: *anyopaque, sel_after: tc.Range) void {
const doc = docFromCtx(ctx);
if (doc.history.end(sdk.allocator(), sel_after)) |c| doc.recordCommitted(c);
// One hand's typing never joins another's run (`sdk.Host.mayMerge`).
const host = sdk.host();
const may_join = if (doc.history.lastApplied()) |top| host.mayMerge(doc.id, top.step) else true;
if (doc.history.end(sdk.allocator(), sel_after, may_join)) |c| {
doc.recordCommitted(c);
host.documentEdited(doc.id);
}
}

const max_text_bytes: usize = 64 * 1024 * 1024;
Expand Down
41 changes: 30 additions & 11 deletions plugins/text/src/textcore/History.zig
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,11 @@ pub fn lastUndone(self: *const History) ?StepRef {
return if (items.len == 0) null else .of(&items[items.len - 1]);
}

/// Commit the pending edit, merging it into the current group when VSCode's rule says to. Returns
/// what it did, or null when there was nothing to commit.
pub fn end(self: *History, gpa: Allocator, sel_after: Range) ?Committed {
/// Commit the pending edit, merging it into the current group when VSCode's rule says to and
/// `may_join` allows: false when the step on top is another hand's (`sdk.Host.mayMerge`), so one
/// hand's typing never joins another's run. Returns what it did, or null when there was nothing to
/// commit.
pub fn end(self: *History, gpa: Allocator, sel_after: Range, may_join: bool) ?Committed {
var p = self.pending orelse return null;
self.pending = null;

Expand Down Expand Up @@ -261,7 +263,7 @@ pub fn end(self: *History, gpa: Allocator, sel_after: Range) ?Committed {

self.clearRedo(gpa);

const can_merge = self.group_open and
const can_merge = may_join and self.group_open and
self.undo_stack.items.len > 0 and
!shouldBreakGroup(prev_kind, kind);

Expand Down Expand Up @@ -473,7 +475,7 @@ const Harness = struct {
try self.text.insertSlice(self.gpa, self.cursor, ch);
self.hist.note(self.gpa, self.cursor, "", ch);
self.cursor += ch.len;
_ = self.hist.end(self.gpa, .collapsed(self.cursor));
_ = self.hist.end(self.gpa, .collapsed(self.cursor), true);
}
}

Expand All @@ -485,7 +487,7 @@ const Harness = struct {
self.hist.note(self.gpa, self.cursor - 1, removed, "");
try self.text.replaceRange(self.gpa, self.cursor - 1, 1, "");
self.cursor -= 1;
_ = self.hist.end(self.gpa, .collapsed(self.cursor));
_ = self.hist.end(self.gpa, .collapsed(self.cursor), true);
}
}

Expand Down Expand Up @@ -584,7 +586,7 @@ test "undo restores the selection, not just an offset" {
hist.begin(.init(6, 11));
hist.note(a, 6, "world", "X");
try text.replaceRange(a, 6, 5, "X");
_ = hist.end(a, .collapsed(7));
_ = hist.end(a, .collapsed(7), true);

const sel = hist.applyUndo(a, &text).?;
try t.expectEqualStrings("hello world", text.items);
Expand Down Expand Up @@ -673,7 +675,7 @@ test "cut-shaped deletion is one undoable step" {
h.hist.note(h.gpa, 6, "world", "");
try h.text.replaceRange(h.gpa, 6, 5, "");
h.cursor = 6;
_ = h.hist.end(h.gpa, .collapsed(6));
_ = h.hist.end(h.gpa, .collapsed(6), true);

try t.expectEqualStrings("hello ", h.text.items);
_ = h.undo();
Expand Down Expand Up @@ -716,13 +718,13 @@ test "a commit says whether it started a step or joined one, and what it added"

hist.begin(.collapsed(0));
hist.note(a, 0, "", "a");
const first = hist.end(a, .collapsed(1)).?;
const first = hist.end(a, .collapsed(1), true).?;
try t.expect(!first.joined);
try t.expectEqual(@as(usize, 1), first.edits.len);

hist.begin(.collapsed(1));
hist.note(a, 1, "", "b");
const second = hist.end(a, .collapsed(2)).?;
const second = hist.end(a, .collapsed(2), true).?;
// Typing runs on: the same step, and only the new keystroke.
try t.expect(second.joined);
try t.expectEqual(first.step, second.step);
Expand All @@ -731,7 +733,7 @@ test "a commit says whether it started a step or joined one, and what it added"

// An empty commit commits nothing.
hist.begin(.collapsed(2));
try t.expect(hist.end(a, .collapsed(2)) == null);
try t.expect(hist.end(a, .collapsed(2), true) == null);

var text: std.ArrayList(u8) = .empty;
defer text.deinit(a);
Expand All @@ -742,3 +744,20 @@ test "a commit says whether it started a step or joined one, and what it added"
_ = hist.applyRedo(a, &text).?;
try t.expectEqual(first.step, hist.lastApplied().?.step);
}

test "an edit another hand may not join starts a step of its own" {
const a = t.allocator;
var hist: History = .{};
defer hist.deinit(a);

hist.begin(.collapsed(0));
hist.note(a, 0, "", "a");
const first = hist.end(a, .collapsed(1), true).?;
hist.begin(.collapsed(1));
hist.note(a, 1, "", "b");
// Typing on, which VSCode's rule would join, but the step on top is another hand's.
const second = hist.end(a, .collapsed(2), false).?;
try t.expect(!second.joined);
try t.expect(second.step != first.step);
try t.expectEqual(@as(usize, 2), hist.undo_stack.items.len);
}
Loading