From b7aa421e4e460f2af0859d2621d40d53a7801411 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 11:10:51 -0800 Subject: [PATCH 1/8] Add TextEditor::withGroupingInterval This method temporarily instructs the editor to apply undo grouping with a given interval. This way, undo grouping can be made optional without adding optional arguments to every buffer manipulation method. --- spec/text-editor-spec.coffee | 30 ++++++++++++++++++++++++++++++ src/text-editor.coffee | 18 ++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 56e86edc6..f74f248fd 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -2816,6 +2816,36 @@ describe "TextEditor", -> editor.redo() expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] + describe ".withGroupingInterval(interval)", -> + currentTime = null + + beforeEach -> + currentTime = 0 + spyOn(Date, 'now').andCallFake -> currentTime + + it "allows undo entries to be grouped", -> + buffer.setText("") + + editor.withGroupingInterval 200, -> + editor.insertText("1") + + currentTime += 199 + editor.insertText("2") + + currentTime += 199 + editor.insertText("3") + + currentTime += 200 + editor.insertText("4") + + expect(buffer.getText()).toBe "1234" + + editor.undo() + expect(buffer.getText()).toBe "123" + + editor.undo() + expect(buffer.getText()).toBe "" + xit "restores folds after undo and redo", -> editor.foldBufferRow(1) editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 812524482..382ebf0a6 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -68,6 +68,7 @@ class TextEditor extends Model suppressSelectionMerging: false updateBatchDepth: 0 selectionFlashDuration: 500 + groupingInterval: 0 @delegatesMethods 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows', 'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows', @@ -1097,7 +1098,7 @@ class TextEditor extends Model # execution and revert any changes performed up to the abortion. # # * `fn` A {Function} to call inside the transaction. - transact: (fn) -> @buffer.transact(fn) + transact: (fn) -> @buffer.transact(fn, @groupingInterval) # Extended: Start an open-ended transaction. # @@ -1105,7 +1106,7 @@ class TextEditor extends Model # transaction. If you nest calls to transactions, only the outermost # transaction is considered. You must match every begin with a matching # commit, but a single call to abort will cancel all nested transactions. - beginTransaction: -> @buffer.beginTransaction() + beginTransaction: -> @buffer.beginTransaction(@groupingInterval) # Extended: Commit an open-ended transaction started with {::beginTransaction} # and push it to the undo stack. @@ -1117,6 +1118,19 @@ class TextEditor extends Model # within the transaction. abortTransaction: -> @buffer.abortTransaction() + # Extended: Set the time interval over which undo/redo operations are grouped. + # + # * `interval` A {Number} of milliseconds within which operations should be + # grouped with respec to undo/redo + # * `fn` A {Function} to call with the given interval setting + withGroupingInterval: (interval, fn) -> + previousInterval = @groupingInterval + @groupingInterval = interval + try + fn() + finally + @groupingInterval = previousInterval + ### Section: TextEditor Coordinates ### From 5437236304fb24fda035751238f1f9cb613eccce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 11:18:20 -0800 Subject: [PATCH 2/8] Use undo grouping in editor command listeners --- spec/text-editor-component-spec.coffee | 13 ++ src/text-editor-component.coffee | 5 +- src/text-editor-element.coffee | 198 +++++++++++++------------ 3 files changed, 117 insertions(+), 99 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 811681648..8c0734b46 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1924,6 +1924,19 @@ describe "TextEditorComponent", -> expect(nextAnimationFrame).toBe noAnimationFrame expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' + it "groups events that occur close together in time into single undo entries", -> + editor.setText("") + componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'z', target: inputNode)) + + componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) + + expect(editor.getText()).toBe "xyz\nxyz" + + editor.undo() + expect(editor.getText()).toBe "" + describe "when IME composition is used to insert international characters", -> inputNode = null diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index a57f2762d..4c13d36bd 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -21,6 +21,7 @@ TextEditorComponent = React.createClass statics: performSyncUpdates: false + groupingInterval: 500 visible: false autoHeight: false @@ -457,7 +458,9 @@ TextEditorComponent = React.createClass selectedLength = inputNode.selectionEnd - inputNode.selectionStart editor.selectLeft() if selectedLength is 1 - inputNode.value = event.data if editor.insertText(event.data) + insertedRange = editor.withGroupingInterval @constructor.groupingInterval, -> + editor.insertText(event.data) + inputNode.value = event.data if insertedRange onVerticalScroll: (scrollTop) -> {editor} = @props diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 4a428178e..a92fd8087 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -134,115 +134,117 @@ class TextEditorElement extends HTMLElement hasFocus: -> this is document.activeElement or @contains(document.activeElement) -stopCommandEventPropagation = (commandListeners) -> +editorEventListeners = (commandListeners) -> newCommandListeners = {} for commandName, commandListener of commandListeners do (commandListener) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() - commandListener.call(this, event) + model = @getModel() + model.withGroupingInterval TextEditorComponent.groupingInterval, -> + commandListener.call(model, event) newCommandListeners -atom.commands.add 'atom-text-editor', stopCommandEventPropagation( - 'core:move-left': -> @getModel().moveLeft() - 'core:move-right': -> @getModel().moveRight() - 'core:select-left': -> @getModel().selectLeft() - 'core:select-right': -> @getModel().selectRight() - 'core:select-all': -> @getModel().selectAll() - 'core:backspace': -> @getModel().backspace() - 'core:delete': -> @getModel().delete() - 'core:undo': -> @getModel().undo() - 'core:redo': -> @getModel().redo() - 'core:cut': -> @getModel().cutSelectedText() - 'core:copy': -> @getModel().copySelectedText() - 'core:paste': -> @getModel().pasteText() - 'editor:move-to-previous-word': -> @getModel().moveToPreviousWord() - 'editor:select-word': -> @getModel().selectWordsContainingCursors() - 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @getModel().consolidateSelections() - 'editor:delete-to-beginning-of-word': -> @getModel().deleteToBeginningOfWord() - 'editor:delete-to-beginning-of-line': -> @getModel().deleteToBeginningOfLine() - 'editor:delete-to-end-of-line': -> @getModel().deleteToEndOfLine() - 'editor:delete-to-end-of-word': -> @getModel().deleteToEndOfWord() - 'editor:delete-line': -> @getModel().deleteLine() - 'editor:cut-to-end-of-line': -> @getModel().cutToEndOfLine() - 'editor:move-to-beginning-of-next-paragraph': -> @getModel().moveToBeginningOfNextParagraph() - 'editor:move-to-beginning-of-previous-paragraph': -> @getModel().moveToBeginningOfPreviousParagraph() - 'editor:move-to-beginning-of-screen-line': -> @getModel().moveToBeginningOfScreenLine() - 'editor:move-to-beginning-of-line': -> @getModel().moveToBeginningOfLine() - 'editor:move-to-end-of-screen-line': -> @getModel().moveToEndOfScreenLine() - 'editor:move-to-end-of-line': -> @getModel().moveToEndOfLine() - 'editor:move-to-first-character-of-line': -> @getModel().moveToFirstCharacterOfLine() - 'editor:move-to-beginning-of-word': -> @getModel().moveToBeginningOfWord() - 'editor:move-to-end-of-word': -> @getModel().moveToEndOfWord() - 'editor:move-to-beginning-of-next-word': -> @getModel().moveToBeginningOfNextWord() - 'editor:move-to-previous-word-boundary': -> @getModel().moveToPreviousWordBoundary() - 'editor:move-to-next-word-boundary': -> @getModel().moveToNextWordBoundary() - 'editor:select-to-beginning-of-next-paragraph': -> @getModel().selectToBeginningOfNextParagraph() - 'editor:select-to-beginning-of-previous-paragraph': -> @getModel().selectToBeginningOfPreviousParagraph() - 'editor:select-to-end-of-line': -> @getModel().selectToEndOfLine() - 'editor:select-to-beginning-of-line': -> @getModel().selectToBeginningOfLine() - 'editor:select-to-end-of-word': -> @getModel().selectToEndOfWord() - 'editor:select-to-beginning-of-word': -> @getModel().selectToBeginningOfWord() - 'editor:select-to-beginning-of-next-word': -> @getModel().selectToBeginningOfNextWord() - 'editor:select-to-next-word-boundary': -> @getModel().selectToNextWordBoundary() - 'editor:select-to-previous-word-boundary': -> @getModel().selectToPreviousWordBoundary() - 'editor:select-to-first-character-of-line': -> @getModel().selectToFirstCharacterOfLine() - 'editor:select-line': -> @getModel().selectLinesContainingCursors() - 'editor:transpose': -> @getModel().transpose() - 'editor:upper-case': -> @getModel().upperCase() - 'editor:lower-case': -> @getModel().lowerCase() +atom.commands.add 'atom-text-editor', editorEventListeners( + 'core:move-left': -> @moveLeft() + 'core:move-right': -> @moveRight() + 'core:select-left': -> @selectLeft() + 'core:select-right': -> @selectRight() + 'core:select-all': -> @selectAll() + 'core:backspace': -> @backspace() + 'core:delete': -> @delete() + 'core:undo': -> @undo() + 'core:redo': -> @redo() + 'core:cut': -> @cutSelectedText() + 'core:copy': -> @copySelectedText() + 'core:paste': -> @pasteText() + 'editor:move-to-previous-word': -> @moveToPreviousWord() + 'editor:select-word': -> @selectWordsContainingCursors() + 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() + 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() + 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() + 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() + 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() + 'editor:delete-line': -> @deleteLine() + 'editor:cut-to-end-of-line': -> @cutToEndOfLine() + 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() + 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() + 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() + 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() + 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() + 'editor:move-to-end-of-line': -> @moveToEndOfLine() + 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() + 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() + 'editor:move-to-end-of-word': -> @moveToEndOfWord() + 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() + 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() + 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() + 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() + 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() + 'editor:select-to-end-of-line': -> @selectToEndOfLine() + 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() + 'editor:select-to-end-of-word': -> @selectToEndOfWord() + 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() + 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() + 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() + 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() + 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() + 'editor:select-line': -> @selectLinesContainingCursors() + 'editor:transpose': -> @transpose() + 'editor:upper-case': -> @upperCase() + 'editor:lower-case': -> @lowerCase() ) -atom.commands.add 'atom-text-editor:not(.mini)', stopCommandEventPropagation( - 'core:move-up': -> @getModel().moveUp() - 'core:move-down': -> @getModel().moveDown() - 'core:move-to-top': -> @getModel().moveToTop() - 'core:move-to-bottom': -> @getModel().moveToBottom() - 'core:page-up': -> @getModel().pageUp() - 'core:page-down': -> @getModel().pageDown() - 'core:select-up': -> @getModel().selectUp() - 'core:select-down': -> @getModel().selectDown() - 'core:select-to-top': -> @getModel().selectToTop() - 'core:select-to-bottom': -> @getModel().selectToBottom() - 'core:select-page-up': -> @getModel().selectPageUp() - 'core:select-page-down': -> @getModel().selectPageDown() - 'editor:indent': -> @getModel().indent() - 'editor:auto-indent': -> @getModel().autoIndentSelectedRows() - 'editor:indent-selected-rows': -> @getModel().indentSelectedRows() - 'editor:outdent-selected-rows': -> @getModel().outdentSelectedRows() - 'editor:newline': -> @getModel().insertNewline() - 'editor:newline-below': -> @getModel().insertNewlineBelow() - 'editor:newline-above': -> @getModel().insertNewlineAbove() - 'editor:add-selection-below': -> @getModel().addSelectionBelow() - 'editor:add-selection-above': -> @getModel().addSelectionAbove() - 'editor:split-selections-into-lines': -> @getModel().splitSelectionsIntoLines() - 'editor:toggle-soft-tabs': -> @getModel().toggleSoftTabs() - 'editor:toggle-soft-wrap': -> @getModel().toggleSoftWrapped() - 'editor:fold-all': -> @getModel().foldAll() - 'editor:unfold-all': -> @getModel().unfoldAll() - 'editor:fold-current-row': -> @getModel().foldCurrentRow() - 'editor:unfold-current-row': -> @getModel().unfoldCurrentRow() - 'editor:fold-selection': -> @getModel().foldSelectedLines() - 'editor:fold-at-indent-level-1': -> @getModel().foldAllAtIndentLevel(0) - 'editor:fold-at-indent-level-2': -> @getModel().foldAllAtIndentLevel(1) - 'editor:fold-at-indent-level-3': -> @getModel().foldAllAtIndentLevel(2) - 'editor:fold-at-indent-level-4': -> @getModel().foldAllAtIndentLevel(3) - 'editor:fold-at-indent-level-5': -> @getModel().foldAllAtIndentLevel(4) - 'editor:fold-at-indent-level-6': -> @getModel().foldAllAtIndentLevel(5) - 'editor:fold-at-indent-level-7': -> @getModel().foldAllAtIndentLevel(6) - 'editor:fold-at-indent-level-8': -> @getModel().foldAllAtIndentLevel(7) - 'editor:fold-at-indent-level-9': -> @getModel().foldAllAtIndentLevel(8) - 'editor:toggle-line-comments': -> @getModel().toggleLineCommentsInSelection() - 'editor:log-cursor-scope': -> @getModel().logCursorScope() - 'editor:checkout-head-revision': -> atom.project.getRepositories()[0]?.checkoutHeadForEditor(@getModel()) - 'editor:copy-path': -> @getModel().copyPathToClipboard() - 'editor:move-line-up': -> @getModel().moveLineUp() - 'editor:move-line-down': -> @getModel().moveLineDown() - 'editor:duplicate-lines': -> @getModel().duplicateLines() - 'editor:join-lines': -> @getModel().joinLines() +atom.commands.add 'atom-text-editor:not(.mini)', editorEventListeners( + 'core:move-up': -> @moveUp() + 'core:move-down': -> @moveDown() + 'core:move-to-top': -> @moveToTop() + 'core:move-to-bottom': -> @moveToBottom() + 'core:page-up': -> @pageUp() + 'core:page-down': -> @pageDown() + 'core:select-up': -> @selectUp() + 'core:select-down': -> @selectDown() + 'core:select-to-top': -> @selectToTop() + 'core:select-to-bottom': -> @selectToBottom() + 'core:select-page-up': -> @selectPageUp() + 'core:select-page-down': -> @selectPageDown() + 'editor:indent': -> @indent() + 'editor:auto-indent': -> @autoIndentSelectedRows() + 'editor:indent-selected-rows': -> @indentSelectedRows() + 'editor:outdent-selected-rows': -> @outdentSelectedRows() + 'editor:newline': -> @insertNewline() + 'editor:newline-below': -> @insertNewlineBelow() + 'editor:newline-above': -> @insertNewlineAbove() + 'editor:add-selection-below': -> @addSelectionBelow() + 'editor:add-selection-above': -> @addSelectionAbove() + 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() + 'editor:toggle-soft-tabs': -> @toggleSoftTabs() + 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() + 'editor:fold-all': -> @foldAll() + 'editor:unfold-all': -> @unfoldAll() + 'editor:fold-current-row': -> @foldCurrentRow() + 'editor:unfold-current-row': -> @unfoldCurrentRow() + 'editor:fold-selection': -> @foldSelectedLines() + 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) + 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) + 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) + 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) + 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) + 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) + 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) + 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) + 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) + 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() + 'editor:log-cursor-scope': -> @logCursorScope() + 'editor:checkout-head-revision': -> atom.project.getRepositories()[0]?.checkoutHeadForEditor(this) + 'editor:copy-path': -> @copyPathToClipboard() + 'editor:move-line-up': -> @moveLineUp() + 'editor:move-line-down': -> @moveLineDown() + 'editor:duplicate-lines': -> @duplicateLines() + 'editor:join-lines': -> @joinLines() 'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide')) 'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers')) - 'editor:scroll-to-cursor': -> @getModel().scrollToCursorPosition() + 'editor:scroll-to-cursor': -> @scrollToCursorPosition() ) module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype From e7eef89fa534ac236f2102f022222d8efae68d3d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 14:35:09 -0800 Subject: [PATCH 3/8] Remove TextEditor::withGroupingInterval Just use ::transact --- spec/text-editor-spec.coffee | 30 ------------------------------ src/text-editor-component.coffee | 3 ++- src/text-editor-element.coffee | 3 ++- src/text-editor.coffee | 25 +++++++++---------------- 4 files changed, 13 insertions(+), 48 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index f74f248fd..56e86edc6 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -2816,36 +2816,6 @@ describe "TextEditor", -> editor.redo() expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - describe ".withGroupingInterval(interval)", -> - currentTime = null - - beforeEach -> - currentTime = 0 - spyOn(Date, 'now').andCallFake -> currentTime - - it "allows undo entries to be grouped", -> - buffer.setText("") - - editor.withGroupingInterval 200, -> - editor.insertText("1") - - currentTime += 199 - editor.insertText("2") - - currentTime += 199 - editor.insertText("3") - - currentTime += 200 - editor.insertText("4") - - expect(buffer.getText()).toBe "1234" - - editor.undo() - expect(buffer.getText()).toBe "123" - - editor.undo() - expect(buffer.getText()).toBe "" - xit "restores folds after undo and redo", -> editor.foldBufferRow(1) editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 4c13d36bd..838692ae3 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -458,8 +458,9 @@ TextEditorComponent = React.createClass selectedLength = inputNode.selectionEnd - inputNode.selectionStart editor.selectLeft() if selectedLength is 1 - insertedRange = editor.withGroupingInterval @constructor.groupingInterval, -> + insertedRange = editor.transact(-> editor.insertText(event.data) + , @constructor.groupingInterval) inputNode.value = event.data if insertedRange onVerticalScroll: (scrollTop) -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index a92fd8087..fca421232 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -141,8 +141,9 @@ editorEventListeners = (commandListeners) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() model = @getModel() - model.withGroupingInterval TextEditorComponent.groupingInterval, -> + model.transact(-> commandListener.call(model, event) + , TextEditorComponent.groupingInterval) newCommandListeners atom.commands.add 'atom-text-editor', editorEventListeners( diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 382ebf0a6..c1f0b9f56 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -68,7 +68,6 @@ class TextEditor extends Model suppressSelectionMerging: false updateBatchDepth: 0 selectionFlashDuration: 500 - groupingInterval: 0 @delegatesMethods 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows', 'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows', @@ -1098,7 +1097,9 @@ class TextEditor extends Model # execution and revert any changes performed up to the abortion. # # * `fn` A {Function} to call inside the transaction. - transact: (fn) -> @buffer.transact(fn, @groupingInterval) + # * `groupingInterval` This is the sames as the `groupingInterval` parameter + # in {::beginTransaction} + transact: (fn, groupingInterval) -> @buffer.transact(fn, groupingInterval) # Extended: Start an open-ended transaction. # @@ -1106,7 +1107,12 @@ class TextEditor extends Model # transaction. If you nest calls to transactions, only the outermost # transaction is considered. You must match every begin with a matching # commit, but a single call to abort will cancel all nested transactions. - beginTransaction: -> @buffer.beginTransaction(@groupingInterval) + # + # * `groupingInterval` (optional) The {Number} of milliseconds for which this + # transaction should be considered 'groupable' after it begins. If a transaction + # with a positive `groupingInterval` is committed while the previous transaction is + # still 'groupable', the two transactions are merged with respect to undo and redo. + beginTransaction: (groupingInterval) -> @buffer.beginTransaction(groupingInterval) # Extended: Commit an open-ended transaction started with {::beginTransaction} # and push it to the undo stack. @@ -1118,19 +1124,6 @@ class TextEditor extends Model # within the transaction. abortTransaction: -> @buffer.abortTransaction() - # Extended: Set the time interval over which undo/redo operations are grouped. - # - # * `interval` A {Number} of milliseconds within which operations should be - # grouped with respec to undo/redo - # * `fn` A {Function} to call with the given interval setting - withGroupingInterval: (interval, fn) -> - previousInterval = @groupingInterval - @groupingInterval = interval - try - fn() - finally - @groupingInterval = previousInterval - ### Section: TextEditor Coordinates ### From 4077e791c9cd6da614dd9eee2569ca4a3bd14e67 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 15:03:02 -0800 Subject: [PATCH 4/8] Update signature of calls to TextBuffer::transact --- src/text-editor-component.coffee | 3 +-- src/text-editor-element.coffee | 3 +-- src/text-editor.coffee | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 838692ae3..117ce168c 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -458,9 +458,8 @@ TextEditorComponent = React.createClass selectedLength = inputNode.selectionEnd - inputNode.selectionStart editor.selectLeft() if selectedLength is 1 - insertedRange = editor.transact(-> + insertedRange = editor.transact @constructor.groupingInterval, -> editor.insertText(event.data) - , @constructor.groupingInterval) inputNode.value = event.data if insertedRange onVerticalScroll: (scrollTop) -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index fca421232..ad2e63043 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -141,9 +141,8 @@ editorEventListeners = (commandListeners) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() model = @getModel() - model.transact(-> + model.transact TextEditorComponent.groupingInterval, -> commandListener.call(model, event) - , TextEditorComponent.groupingInterval) newCommandListeners atom.commands.add 'atom-text-editor', editorEventListeners( diff --git a/src/text-editor.coffee b/src/text-editor.coffee index c1f0b9f56..6e8ab4922 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1096,10 +1096,10 @@ class TextEditor extends Model # abort the transaction, call {::abortTransaction} to terminate the function's # execution and revert any changes performed up to the abortion. # + # * `groupingInterval` (optional) This is the sames as the `groupingInterval` + # parameter in {::beginTransaction} # * `fn` A {Function} to call inside the transaction. - # * `groupingInterval` This is the sames as the `groupingInterval` parameter - # in {::beginTransaction} - transact: (fn, groupingInterval) -> @buffer.transact(fn, groupingInterval) + transact: (groupingInterval, fn) -> @buffer.transact(groupingInterval, fn) # Extended: Start an open-ended transaction. # From d85c07e7e2a6acef7d139a28903c16a543493516 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 16:59:06 -0800 Subject: [PATCH 5/8] Don't wrap undo/redo calls in transactions --- spec/text-editor-component-spec.coffee | 2 +- src/text-editor-element.coffee | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 8c0734b46..6976acea0 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1934,7 +1934,7 @@ describe "TextEditorComponent", -> expect(editor.getText()).toBe "xyz\nxyz" - editor.undo() + componentNode.dispatchEvent(new CustomEvent('core:undo', bubbles: true, cancelable: true)) expect(editor.getText()).toBe "" describe "when IME composition is used to insert international characters", -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index ad2e63043..3ad46ffed 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -134,7 +134,16 @@ class TextEditorElement extends HTMLElement hasFocus: -> this is document.activeElement or @contains(document.activeElement) -editorEventListeners = (commandListeners) -> +stopEventPropagation = (commandListeners) -> + newCommandListeners = {} + for commandName, commandListener of commandListeners + do (commandListener) -> + newCommandListeners[commandName] = (event) -> + event.stopPropagation() + commandListener.call(@getModel(), event) + newCommandListeners + +stopEventPropagationAndGroupUndo = (commandListeners) -> newCommandListeners = {} for commandName, commandListener of commandListeners do (commandListener) -> @@ -145,7 +154,12 @@ editorEventListeners = (commandListeners) -> commandListener.call(model, event) newCommandListeners -atom.commands.add 'atom-text-editor', editorEventListeners( +atom.commands.add 'atom-text-editor', stopEventPropagation( + 'core:undo': -> @undo() + 'core:redo': -> @redo() +) + +atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo( 'core:move-left': -> @moveLeft() 'core:move-right': -> @moveRight() 'core:select-left': -> @selectLeft() @@ -153,8 +167,6 @@ atom.commands.add 'atom-text-editor', editorEventListeners( 'core:select-all': -> @selectAll() 'core:backspace': -> @backspace() 'core:delete': -> @delete() - 'core:undo': -> @undo() - 'core:redo': -> @redo() 'core:cut': -> @cutSelectedText() 'core:copy': -> @copySelectedText() 'core:paste': -> @pasteText() @@ -195,7 +207,7 @@ atom.commands.add 'atom-text-editor', editorEventListeners( 'editor:lower-case': -> @lowerCase() ) -atom.commands.add 'atom-text-editor:not(.mini)', editorEventListeners( +atom.commands.add 'atom-text-editor:not(.mini)', stopEventPropagationAndGroupUndo( 'core:move-up': -> @moveUp() 'core:move-down': -> @moveDown() 'core:move-to-top': -> @moveToTop() From d97c81bf6a4833f125f36e0a02b032dd86117fe5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Nov 2014 09:25:10 -0800 Subject: [PATCH 6/8] Make undo grouping interval configurable --- spec/text-editor-component-spec.coffee | 18 +++++++++++++++--- src/config-schema.coffee | 5 +++++ src/text-editor-component.coffee | 3 +-- src/text-editor-element.coffee | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 6976acea0..030bba37d 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1925,14 +1925,26 @@ describe "TextEditorComponent", -> expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' it "groups events that occur close together in time into single undo entries", -> + currentTime = 0 + spyOn(Date, 'now').andCallFake -> currentTime + + atom.config.set('editor.undoGroupingInterval', 100) + editor.setText("") componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: 'z', target: inputNode)) + currentTime += 99 + componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) + + currentTime += 99 componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) - expect(editor.getText()).toBe "xyz\nxyz" + currentTime += 100 + componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) + expect(editor.getText()).toBe "xy\nxy\nxy" + + componentNode.dispatchEvent(new CustomEvent('core:undo', bubbles: true, cancelable: true)) + expect(editor.getText()).toBe "xy\nxy" componentNode.dispatchEvent(new CustomEvent('core:undo', bubbles: true, cancelable: true)) expect(editor.getText()).toBe "" diff --git a/src/config-schema.coffee b/src/config-schema.coffee index afb1af4ce..49805eaf4 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -105,6 +105,11 @@ module.exports = scrollPastEnd: type: 'boolean' default: false + undoGroupingInterval: + type: 'integer' + default: 500 + minimum: 0 + description: 'Time interval in milliseconds within which operations will be grouped in the undo history' useHardwareAcceleration: type: 'boolean' default: true diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 117ce168c..c6e1af08b 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -21,7 +21,6 @@ TextEditorComponent = React.createClass statics: performSyncUpdates: false - groupingInterval: 500 visible: false autoHeight: false @@ -458,7 +457,7 @@ TextEditorComponent = React.createClass selectedLength = inputNode.selectionEnd - inputNode.selectionStart editor.selectLeft() if selectedLength is 1 - insertedRange = editor.transact @constructor.groupingInterval, -> + insertedRange = editor.transact atom.config.get('editor.undoGroupingInterval'), -> editor.insertText(event.data) inputNode.value = event.data if insertedRange diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 3ad46ffed..377aa2a55 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -150,7 +150,7 @@ stopEventPropagationAndGroupUndo = (commandListeners) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() model = @getModel() - model.transact TextEditorComponent.groupingInterval, -> + model.transact atom.config.get('editor.undoGroupingInterval'), -> commandListener.call(model, event) newCommandListeners From 26983adbdf972435f101edd9ce6a762cbec1a5c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Nov 2014 09:42:20 -0800 Subject: [PATCH 7/8] Reduce ambiguity in config description --- src/config-schema.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 49805eaf4..bf41e5a6f 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -109,7 +109,7 @@ module.exports = type: 'integer' default: 500 minimum: 0 - description: 'Time interval in milliseconds within which operations will be grouped in the undo history' + description: 'Time interval in milliseconds within which operations will be grouped together in the undo history' useHardwareAcceleration: type: 'boolean' default: true From 4099828525f64cdbc7503d112cce664720247e25 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 6 Nov 2014 09:55:31 -0800 Subject: [PATCH 8/8] :arrow_up: text-buffer@3.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c8d23fd7..1e4d99a7c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "serializable": "^1", "space-pen": "3.8.0", "temp": "0.7.0", - "text-buffer": "^3.5.1", + "text-buffer": "^3.6.0", "theorist": "^1.0.2", "underscore-plus": "^1.6.1", "vm-compatibility-layer": "0.1.0"