From 5437236304fb24fda035751238f1f9cb613eccce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Nov 2014 11:18:20 -0800 Subject: [PATCH] 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