Skip to content
Open
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
115 changes: 115 additions & 0 deletions spec/text-buffer-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,20 @@ describe("TextBuffer", function() {
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");
});

it("groups all operations in the given function in a single async transaction", async () => {
await buffer.transact(async () => {
await delay(10)
buffer.setTextInRange([[0, 2], [0, 5]], "y");
buffer.transact(() => buffer.setTextInRange([[2, 13], [2, 14]], "igg"));
});

expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?");
buffer.undo();
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");
buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");
});

it("halts execution of the function if the transaction is aborted", function() {
let innerContinued = false;
let outerContinued = false;
Expand All @@ -831,6 +845,28 @@ describe("TextBuffer", function() {
expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?");
});

it("halts execution of the function if the async transaction is aborted", async () => {
let innerContinued = false;
let outerContinued = false;

await buffer.transact(async () => {
await delay(10)
buffer.setTextInRange([[0, 2], [0, 5]], "y");
await buffer.transact(async () => {
buffer.setTextInRange([[2, 13], [2, 14]], "igg");
await delay(10)
buffer.abortTransaction();
innerContinued = true;
});
outerContinued = true;
});

expect(innerContinued).toBe(false);
expect(outerContinued).toBe(true);
expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?");
});


it("groups all operations performed within the given function into a single undo/redo operation", function() {
buffer.transact(function() {
buffer.setTextInRange([[0, 2], [0, 5]], "y");
Expand Down Expand Up @@ -864,6 +900,41 @@ describe("TextBuffer", function() {
expect(buffer.getText()).toBe("hey\nlittle worms\r\nhow are you digging?");
});

it("groups all operations performed within the given async function into a single undo/redo operation", async () => {
await buffer.transact(async () => {
await delay(20)
buffer.setTextInRange([[0, 2], [0, 5]], "y");
await delay(20)
buffer.setTextInRange([[2, 13], [2, 14]], "igg");
});

expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?");

// subsequent changes are not included in the transaction
buffer.setTextInRange([[1, 0], [1, 0]], "little ");
buffer.undo();
expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?");

// this should undo all changes in the transaction
buffer.undo();
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");

// previous changes are not included in the transaction
buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");

buffer.redo();
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");

// this should redo all changes in the transaction
buffer.redo();
expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?");

// this should redo the change following the transaction
buffer.redo();
expect(buffer.getText()).toBe("hey\nlittle worms\r\nhow are you digging?");
});

it("does not push the transaction to the undo stack if it is empty", function() {
buffer.transact(function() {});
buffer.undo();
Expand All @@ -875,6 +946,20 @@ describe("TextBuffer", function() {
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");
});

it("does not push the async transaction to the undo stack if it is empty", async () => {
await buffer.transact(async () => { await delay(20) });
buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");

buffer.redo();
await buffer.transact(async () => {
await(20)
buffer.abortTransaction()
});
buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");
});

it("halts execution undoes all operations since the beginning of the transaction if ::abortTransaction() is called", function() {
let continuedPastAbort = false;
buffer.transact(function() {
Expand All @@ -898,6 +983,30 @@ describe("TextBuffer", function() {
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");
});

it("halts execution undoes all operations since the beginning of the async transaction if ::abortTransaction() is called", async () => {
let continuedPastAbort = false;
await buffer.transact(async () => {
await delay(20)
buffer.setTextInRange([[0, 2], [0, 5]], "y");
buffer.setTextInRange([[2, 13], [2, 14]], "igg");
buffer.abortTransaction();
continuedPastAbort = true;
});

expect(continuedPastAbort).toBe(false);

expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");

buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");

buffer.redo();
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");

buffer.redo();
expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?");
});

it("preserves the redo stack until a content change occurs", function() {
buffer.undo();
expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?");
Expand Down Expand Up @@ -3307,3 +3416,9 @@ function assertChangesEqual(actualChanges, expectedChanges) {
expect(actualChange.newText).toEqual(expectedChange.newText);
}
}

function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
41 changes: 27 additions & 14 deletions src/text-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1363,28 +1363,41 @@ class TextBuffer {
isBarrier: true
})

const catchError = (exception) => {
this.revertToCheckpoint(checkpointBefore, {deleteCheckpoint: true})
if (!(exception instanceof TransactionAbortedError)) throw exception
}
try {
this.transactCallDepth++
result = fn()
} catch (exception) {
this.revertToCheckpoint(checkpointBefore, {deleteCheckpoint: true})
if (!(exception instanceof TransactionAbortedError)) throw exception
return
return catchError(exception)
} finally {
this.transactCallDepth--
if (result?.finally) {
result.finally(() => this.transactCallDepth--).catch(() => {})
} else {
this.transactCallDepth--
}
}

if (this.isDestroyed()) return result
const endMarkerSnapshot = this.createMarkerSnapshot(selectionsMarkerLayer)
this.historyProvider.groupChangesSinceCheckpoint(checkpointBefore, {
markers: endMarkerSnapshot,
deleteCheckpoint: true
})
this.historyProvider.applyGroupingInterval(groupingInterval)
this.historyProvider.enforceUndoStackSizeLimit()
this.emitDidChangeTextEvent()
this.emitMarkerChangeEvents(endMarkerSnapshot)
return result
const afterTransaction = () => {
const endMarkerSnapshot = this.createMarkerSnapshot(selectionsMarkerLayer)
this.historyProvider.groupChangesSinceCheckpoint(checkpointBefore, {
markers: endMarkerSnapshot,
deleteCheckpoint: true
})
this.historyProvider.applyGroupingInterval(groupingInterval)
this.historyProvider.enforceUndoStackSizeLimit()
this.emitDidChangeTextEvent()
this.emitMarkerChangeEvents(endMarkerSnapshot)
return result
}
if(result?.then) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(result?.then) {
if (result?.then) {

return result.then(afterTransaction).catch(catchError)
} else {
return afterTransaction()
}
}

// Public: Abort the currently running transaction
Expand Down
Loading