From a8c4943d9102c6d076656e3a7076ea883c5fb229 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Sun, 1 Mar 2015 17:17:17 -0800 Subject: [PATCH 1/6] Add subword navigation - Add commands for moving, selecting, and deleting camelCase words --- spec/text-editor-spec.coffee | 274 +++++++++++++++++++++++++++++++++ src/cursor.coffee | 34 ++++ src/selection.coffee | 20 +++ src/text-editor-element.coffee | 6 + src/text-editor.coffee | 34 ++++ 5 files changed, 368 insertions(+) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index c03990bf2..c2d46c569 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4071,3 +4071,277 @@ describe "TextEditor", -> editor.checkoutHeadRevision() waitsForPromise -> editor.checkoutHeadRevision() + + describe ".moveToPreviousSubwordBoundary", -> + it 'does not change an empty file', -> + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "traverses normal words", -> + editor.setText("_word \n") + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 5]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText(" word\n") + editor.setCursorBufferPosition([0, 3]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "traverses camelCase words", -> + editor.setText(" getPreviousWord\n") + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "traverses consecutive non-word characters", -> + editor.setText("e, => \n") + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "traverses consecutive uppercase characters", -> + editor.setText(" AAADF \n") + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText("ALPhA\n") + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + + it "traverses consecutive numbers", -> + editor.setText(" 88 \n") + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + describe "when 2 cursors", -> + it "traverses both camelCase words", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveToPreviousSubwordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + + describe ".moveToNextSubwordBoundary", -> + it 'does not change an empty file', -> + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "traverses normal words", -> + editor.setText(" word_ \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 5]) + + editor.setText("word \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + it "traverses camelCase words", -> + editor.setText("getPreviousWord \n") + editor.setCursorBufferPosition([0, 0]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + + it "traverses consecutive non-word characters", -> + editor.setText(", => \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + it "traverses consecutive uppercase characters", -> + editor.setText(" AAADF \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.setText("ALPhA\n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + + it "traverses consecutive numbers", -> + editor.setText(" 88 \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + describe "when 2 cursors", -> + it "traverses both camelCase words", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + + describe ".selectToPreviousSubwordBoundary", -> + it "selects subwords", -> + editor.setText("") + editor.insertText("_word\n") + editor.insertText(" getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0,5]) + editor.addCursorAtBufferPosition([1,7]) + editor.addCursorAtBufferPosition([2,5]) + editor.addCursorAtBufferPosition([3,3]) + [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToPreviousSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0,1], [0,5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1,4], [1,7]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[2,3], [2,5]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3,1], [3,3]]) + expect(selection4.isReversed()).toBeTruthy() + + describe ".selectToNextSubwordBoundary", -> + it "selects subwords", -> + editor.setText("") + editor.insertText("word_\n") + editor.insertText("getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0,1]) + editor.addCursorAtBufferPosition([1,7]) + editor.addCursorAtBufferPosition([2,2]) + editor.addCursorAtBufferPosition([3,1]) + [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToNextSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0,1], [0,4]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[1,7], [1,11]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[2,2], [2,5]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3,1], [3,3]]) + expect(selection4.isReversed()).toBeFalsy() + + describe ".deleteToBeginningOfSubword", -> + it "deletes subwords", -> + editor.setText("") + editor.insertText("_word\n") + editor.insertText(" getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0,5]) + editor.addCursorAtBufferPosition([1,7]) + editor.addCursorAtBufferPosition([2,5]) + editor.addCursorAtBufferPosition([3,3]) + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe(' getviousWord') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0,1]) + expect(cursor2.getBufferPosition()).toEqual([1,4]) + expect(cursor3.getBufferPosition()).toEqual([2,3]) + expect(cursor4.getBufferPosition()).toEqual([3,1]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe(' viousWord') + expect(buffer.lineForRow(2)).toBe('e ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0,0]) + expect(cursor2.getBufferPosition()).toEqual([1,1]) + expect(cursor3.getBufferPosition()).toEqual([2,1]) + expect(cursor4.getBufferPosition()).toEqual([3,0]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('viousWord') + expect(buffer.lineForRow(2)).toBe(' ') + expect(buffer.lineForRow(3)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0,0]) + expect(cursor2.getBufferPosition()).toEqual([1,0]) + expect(cursor3.getBufferPosition()).toEqual([2,0]) + expect(cursor4.getBufferPosition()).toEqual([2,1]) + + describe ".deleteToEndOfSubword", -> + it "deletes subwords", -> + editor.setText("") + editor.insertText("word_\n") + editor.insertText("getPreviousWord \n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0,0]) + editor.addCursorAtBufferPosition([1,0]) + editor.addCursorAtBufferPosition([2,2]) + editor.addCursorAtBufferPosition([3,0]) + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe('PreviousWord ') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe('88 ') + expect(cursor1.getBufferPosition()).toEqual([0,0]) + expect(cursor2.getBufferPosition()).toEqual([1,0]) + expect(cursor3.getBufferPosition()).toEqual([2,2]) + expect(cursor4.getBufferPosition()).toEqual([3,0]) + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('Word ') + expect(buffer.lineForRow(1)).toBe('e, ') + expect(buffer.lineForRow(2)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0,0]) + expect(cursor2.getBufferPosition()).toEqual([0,0]) + expect(cursor3.getBufferPosition()).toEqual([1,2]) + expect(cursor4.getBufferPosition()).toEqual([1,2]) diff --git a/src/cursor.coffee b/src/cursor.coffee index 33f50ca9c..7126eded1 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -407,6 +407,21 @@ class Cursor extends Model if position = @getNextWordBoundaryBufferPosition() @setBufferPosition(position) + # Public: Moves the cursor to the previous subword boundary. + moveToPreviousSubwordBoundary: -> + options = {wordRegex: @subwordRegExp(backwards: true)} + if position = @getPreviousWordBoundaryBufferPosition(options) + # HACK: to fix going left on first line + if position.isEqual(@getBufferPosition()) + position = new Point(position.row, 0) + @setBufferPosition(position) + + # Public: Moves the cursor to the next subword boundary. + moveToNextSubwordBoundary: -> + options = {wordRegex: @subwordRegExp()} + if position = @getNextWordBoundaryBufferPosition(options) + @setBufferPosition(position) + # Public: Moves the cursor to the beginning of the buffer line, skipping all # whitespace. skipLeadingWhitespace: -> @@ -650,6 +665,25 @@ class Cursor extends Model segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") new RegExp(segments.join("|"), "g") + # Public: Get the RegExp used by the cursor to determine what a "subword" is. + # + # * `options` (optional) {Object} with the following keys: + # * `backwards` A {Boolean} indicating whether to look forwards or backwards + # for the next subword. (default: false) + # + # Returns a {RegExp}. + subwordRegExp: (options={}) -> + nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) + segments = ["^[\t ]*$"] + segments.push("[A-Z]?[a-z]+") + segments.push("[A-Z]+(?![a-z])") + segments.push("\\d+") + if options.backwards + segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*") + else + segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+") + new RegExp(segments.join("|"), "g") + ### Section: Private ### diff --git a/src/selection.coffee b/src/selection.coffee index 7c7fad10d..a1385dffb 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -291,6 +291,14 @@ class Selection extends Model selectToNextWordBoundary: -> @modifySelection => @cursor.moveToNextWordBoundary() + # Public: Selects text to the previous subword boundary. + selectToPreviousSubwordBoundary: -> + @modifySelection => @cursor.moveToPreviousSubwordBoundary() + + # Public: Selects text to the next subword boundary. + selectToNextSubwordBoundary: -> + @modifySelection => @cursor.moveToNextSubwordBoundary() + # Public: Selects all the text from the current cursor position to the # beginning of the next paragraph. selectToBeginningOfNextParagraph: -> @@ -454,6 +462,18 @@ class Selection extends Model @selectToEndOfWord() if @isEmpty() @deleteSelectedText() + # Public: Removes the selection or all characters from the start of the + # selection to the end of the current word if nothing is selected. + deleteToBeginningOfSubword: -> + @selectToPreviousSubwordBoundary() if @isEmpty() + @deleteSelectedText() + + # Public: Removes the selection or all characters from the start of the + # selection to the end of the current word if nothing is selected. + deleteToEndOfSubword: -> + @selectToNextSubwordBoundary() if @isEmpty() + @deleteSelectedText() + # Public: Removes only the selected text. deleteSelectedText: -> bufferRange = @getBufferRange() diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index ccd69dca8..c9201bab5 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -259,6 +259,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagation( 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() + 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() + 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() 'editor:select-to-end-of-line': -> @selectToEndOfLine() @@ -268,6 +270,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagation( '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-next-subword-boundary': -> @selectToNextSubwordBoundary() + 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() 'editor:select-line': -> @selectLinesContainingCursors() ) @@ -282,6 +286,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo( 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() + 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() + 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() 'editor:delete-line': -> @deleteLine() 'editor:cut-to-end-of-line': -> @cutToEndOfLine() 'editor:transpose': -> @transpose() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b44b6e276..6f010b0ea 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1083,6 +1083,18 @@ class TextEditor extends Model deleteToBeginningOfWord: -> @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() + # Extended: For each selection, if the selection is empty, delete all characters + # of the containing subword following the cursor. Otherwise delete the selected + # text. + deleteToBeginningOfSubword: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfSubword() + + # Extended: For each selection, if the selection is empty, delete all characters + # of the containing subword following the cursor. Otherwise delete the selected + # text. + deleteToEndOfSubword: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfSubword() + # Extended: For each selection, if the selection is empty, delete all characters # of the containing line that precede the cursor. Otherwise delete the # selected text. @@ -1733,6 +1745,14 @@ class TextEditor extends Model deprecate("Use TextEditor::moveToNextWordBoundary() instead") @moveToNextWordBoundary() + # Extended: Move every cursor to the previous subword boundary. + moveToPreviousSubwordBoundary: -> + @moveCursors (cursor) -> cursor.moveToPreviousSubwordBoundary() + + # Extended: Move every cursor to the next subword boundary. + moveToNextSubwordBoundary: -> + @moveCursors (cursor) -> cursor.moveToNextSubwordBoundary() + # Extended: Move every cursor to the beginning of the next paragraph. moveToBeginningOfNextParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() @@ -2061,6 +2081,20 @@ class TextEditor extends Model selectToEndOfWord: -> @expandSelectionsForward (selection) -> selection.selectToEndOfWord() + # Extended: For each selection, move its cursor to the preceding subword + # boundary while maintaining the selection's tail position. + # + # This method may merge selections that end up intersecting. + selectToPreviousSubwordBoundary: -> + @expandSelectionsBackward (selection) -> selection.selectToPreviousSubwordBoundary() + + # Extended: For each selection, move its cursor to the next subword boundary + # while maintaining the selection's tail position. + # + # This method may merge selections that end up intersecting. + selectToNextSubwordBoundary: -> + @expandSelectionsForward (selection) -> selection.selectToNextSubwordBoundary() + # Essential: For each cursor, select the containing line. # # This method merges selections on successive lines. From c8b4129b3161397f4adc72d739d7e273d0315758 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Sun, 1 Mar 2015 17:27:54 -0800 Subject: [PATCH 2/6] Add keymaps --- keymaps/darwin.cson | 6 ++++++ keymaps/linux.cson | 6 ++++++ keymaps/win32.cson | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 5226bb5e6..79e33a8c8 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -135,6 +135,12 @@ 'cmd-k cmd-l': 'editor:lower-case' 'cmd-l': 'editor:select-line' 'ctrl-t': 'editor:transpose' + 'ctrl-alt-left': 'editor:move-to-previous-subword-boundary' + 'ctrl-alt-right': 'editor:move-to-next-subword-boundary' + 'ctrl-alt-shift-left': 'editor:select-to-previous-subword-boundary' + 'ctrl-alt-shift-right': 'editor:select-to-next-subword-boundary' + 'ctrl-alt-backspace': 'editor:delete-to-beginning-of-subword' + 'ctrl-alt-delete': 'editor:delete-to-end-of-subword' 'atom-workspace atom-text-editor:not([mini])': # Atom specific diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 59803d193..6e13d70a0 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -93,6 +93,12 @@ 'ctrl-end': 'core:move-to-bottom' 'ctrl-shift-home': 'core:select-to-top' 'ctrl-shift-end': 'core:select-to-bottom' + 'alt-left': 'editor:move-to-previous-subword-boundary' + 'alt-right': 'editor:move-to-next-subword-boundary' + 'alt-shift-left': 'editor:select-to-previous-subword-boundary' + 'alt-shift-right': 'editor:select-to-next-subword-boundary' + 'alt-backspace': 'editor:delete-to-beginning-of-subword' + 'alt-delete': 'editor:delete-to-end-of-subword' # Sublime Parity 'ctrl-a': 'core:select-all' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index da43ac364..e26aea717 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -90,6 +90,12 @@ 'ctrl-end': 'core:move-to-bottom' 'ctrl-shift-home': 'core:select-to-top' 'ctrl-shift-end': 'core:select-to-bottom' + 'alt-left': 'editor:move-to-previous-subword-boundary' + 'alt-right': 'editor:move-to-next-subword-boundary' + 'alt-shift-left': 'editor:select-to-previous-subword-boundary' + 'alt-shift-right': 'editor:select-to-next-subword-boundary' + 'alt-backspace': 'editor:delete-to-beginning-of-subword' + 'alt-delete': 'editor:delete-to-end-of-subword' # Sublime Parity 'ctrl-a': 'core:select-all' From eae9a455793d4a7fa4d68646784097ac92986475 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Jun 2015 15:54:15 -0500 Subject: [PATCH 3/6] :art: Clean up spec language --- spec/text-editor-spec.coffee | 58 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 4ed605409..8bbf722c2 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4364,12 +4364,12 @@ describe "TextEditor", -> waitsForPromise -> editor.checkoutHeadRevision() describe ".moveToPreviousSubwordBoundary", -> - it 'does not change an empty file', -> + it "does not move the cursor when there is no previous subword boundary", -> editor.setText('') editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - it "traverses normal words", -> + it "stops at word and underscore boundaries", -> editor.setText("_word \n") editor.setCursorBufferPosition([0, 6]) editor.moveToPreviousSubwordBoundary() @@ -4383,7 +4383,7 @@ describe "TextEditor", -> editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - it "traverses camelCase words", -> + it "stops at camelCase boundaries", -> editor.setText(" getPreviousWord\n") editor.setCursorBufferPosition([0, 16]) @@ -4396,7 +4396,7 @@ describe "TextEditor", -> editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - it "traverses consecutive non-word characters", -> + it "skips consecutive non-word characters", -> editor.setText("e, => \n") editor.setCursorBufferPosition([0, 6]) editor.moveToPreviousSubwordBoundary() @@ -4405,7 +4405,7 @@ describe "TextEditor", -> editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - it "traverses consecutive uppercase characters", -> + it "skips consecutive uppercase characters", -> editor.setText(" AAADF \n") editor.setCursorBufferPosition([0, 7]) editor.moveToPreviousSubwordBoundary() @@ -4419,7 +4419,7 @@ describe "TextEditor", -> editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - it "traverses consecutive numbers", -> + it "skips consecutive numbers", -> editor.setText(" 88 \n") editor.setCursorBufferPosition([0, 4]) editor.moveToPreviousSubwordBoundary() @@ -4428,25 +4428,24 @@ describe "TextEditor", -> editor.moveToPreviousSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - describe "when 2 cursors", -> - it "traverses both camelCase words", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 8]) - editor.addCursorAtBufferPosition([1, 13]) - [cursor1, cursor2] = editor.getCursors() + it "works with multiple cursors", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + [cursor1, cursor2] = editor.getCursors() - editor.moveToPreviousSubwordBoundary() + editor.moveToPreviousSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) describe ".moveToNextSubwordBoundary", -> - it 'does not change an empty file', -> + it "does not move the cursor when there is no next subword boundary", -> editor.setText('') editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - it "traverses normal words", -> + it "stops at word and underscore boundaries", -> editor.setText(" word_ \n") editor.setCursorBufferPosition([0, 0]) editor.moveToNextSubwordBoundary() @@ -4460,7 +4459,7 @@ describe "TextEditor", -> editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - it "traverses camelCase words", -> + it "stops at camelCase boundaries", -> editor.setText("getPreviousWord \n") editor.setCursorBufferPosition([0, 0]) @@ -4473,7 +4472,7 @@ describe "TextEditor", -> editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - it "traverses consecutive non-word characters", -> + it "skips consecutive non-word characters", -> editor.setText(", => \n") editor.setCursorBufferPosition([0, 0]) editor.moveToNextSubwordBoundary() @@ -4482,7 +4481,7 @@ describe "TextEditor", -> editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - it "traverses consecutive uppercase characters", -> + it "skips consecutive uppercase characters", -> editor.setText(" AAADF \n") editor.setCursorBufferPosition([0, 0]) editor.moveToNextSubwordBoundary() @@ -4496,7 +4495,7 @@ describe "TextEditor", -> editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - it "traverses consecutive numbers", -> + it "skips consecutive numbers", -> editor.setText(" 88 \n") editor.setCursorBufferPosition([0, 0]) editor.moveToNextSubwordBoundary() @@ -4505,16 +4504,15 @@ describe "TextEditor", -> editor.moveToNextSubwordBoundary() expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - describe "when 2 cursors", -> - it "traverses both camelCase words", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() + it "works with multiple cursors", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + [cursor1, cursor2] = editor.getCursors() - editor.moveToNextSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) describe ".selectToPreviousSubwordBoundary", -> it "selects subwords", -> From ba3ab41f1fe83b9ebca766bc0cf3a4962ac6f43a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Jun 2015 22:52:35 -0500 Subject: [PATCH 4/6] Eliminate hack to move cursor to beginning of buffer Use the structure of the regex plus a fix to text-buffer instead. --- package.json | 2 +- spec/text-editor-spec.coffee | 13 +++++++------ src/cursor.coffee | 17 +++++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 8cca44bf7..6c667f3c9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", - "text-buffer": "6.3.7", + "text-buffer": "6.3.8", "theorist": "^1.0.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 8bbf722c2..641431d5a 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4627,13 +4627,14 @@ describe "TextEditor", -> expect(cursor4.getBufferPosition()).toEqual([3,0]) editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('Word ') - expect(buffer.lineForRow(1)).toBe('e, ') - expect(buffer.lineForRow(2)).toBe('') + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('Word ') + expect(buffer.lineForRow(2)).toBe('e,') + expect(buffer.lineForRow(3)).toBe(' ') expect(cursor1.getBufferPosition()).toEqual([0,0]) - expect(cursor2.getBufferPosition()).toEqual([0,0]) - expect(cursor3.getBufferPosition()).toEqual([1,2]) - expect(cursor4.getBufferPosition()).toEqual([1,2]) + expect(cursor2.getBufferPosition()).toEqual([1,0]) + expect(cursor3.getBufferPosition()).toEqual([2,2]) + expect(cursor4.getBufferPosition()).toEqual([3,0]) describe 'gutters', -> describe 'the TextEditor constructor', -> diff --git a/src/cursor.coffee b/src/cursor.coffee index b6b07dbf2..61f12cc8c 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -403,9 +403,6 @@ class Cursor extends Model moveToPreviousSubwordBoundary: -> options = {wordRegex: @subwordRegExp(backwards: true)} if position = @getPreviousWordBoundaryBufferPosition(options) - # HACK: to fix going left on first line - if position.isEqual(@getBufferPosition()) - position = new Point(position.row, 0) @setBufferPosition(position) # Public: Moves the cursor to the next subword boundary. @@ -448,7 +445,7 @@ class Cursor extends Model getPreviousWordBoundaryBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) - scanRange = [[previousNonBlankRow, 0], currentBufferPosition] + scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition] beginningOfWordPosition = null @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> @@ -660,10 +657,14 @@ class Cursor extends Model # Returns a {RegExp}. subwordRegExp: (options={}) -> nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) - segments = ["^[\t ]*$"] - segments.push("[A-Z]?[a-z]+") - segments.push("[A-Z]+(?![a-z])") - segments.push("\\d+") + segments = [ + "^[\t ]+", + "[\t ]+$", + "[A-Z]?[a-z]+", + "[A-Z]+(?![a-z])", + "\\d+", + "_+" + ] if options.backwards segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*") else From df6ef94b6057a53f4fda9ae15d0a155f4a6ed2a9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Jun 2015 23:03:55 -0500 Subject: [PATCH 5/6] :art: add spaces after commas --- spec/text-editor-spec.coffee | 88 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index c47939bf8..a2ade3753 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4522,20 +4522,20 @@ describe "TextEditor", -> editor.insertText(" getPreviousWord\n") editor.insertText("e, => \n") editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0,5]) - editor.addCursorAtBufferPosition([1,7]) - editor.addCursorAtBufferPosition([2,5]) - editor.addCursorAtBufferPosition([3,3]) + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) [selection1, selection2, selection3, selection4] = editor.getSelections() editor.selectToPreviousSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0,1], [0,5]]) + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual([[1,4], [1,7]]) + expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual([[2,3], [2,5]]) + expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual([[3,1], [3,3]]) + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) expect(selection4.isReversed()).toBeTruthy() describe ".selectToNextSubwordBoundary", -> @@ -4545,20 +4545,20 @@ describe "TextEditor", -> editor.insertText("getPreviousWord\n") editor.insertText("e, => \n") editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0,1]) - editor.addCursorAtBufferPosition([1,7]) - editor.addCursorAtBufferPosition([2,2]) - editor.addCursorAtBufferPosition([3,1]) + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 1]) [selection1, selection2, selection3, selection4] = editor.getSelections() editor.selectToNextSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0,1], [0,4]]) + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual([[1,7], [1,11]]) + expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual([[2,2], [2,5]]) + expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual([[3,1], [3,3]]) + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) expect(selection4.isReversed()).toBeFalsy() describe ".deleteToBeginningOfSubword", -> @@ -4568,10 +4568,10 @@ describe "TextEditor", -> editor.insertText(" getPreviousWord\n") editor.insertText("e, => \n") editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0,5]) - editor.addCursorAtBufferPosition([1,7]) - editor.addCursorAtBufferPosition([2,5]) - editor.addCursorAtBufferPosition([3,3]) + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() editor.deleteToBeginningOfSubword() @@ -4579,30 +4579,30 @@ describe "TextEditor", -> expect(buffer.lineForRow(1)).toBe(' getviousWord') expect(buffer.lineForRow(2)).toBe('e, ') expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0,1]) - expect(cursor2.getBufferPosition()).toEqual([1,4]) - expect(cursor3.getBufferPosition()).toEqual([2,3]) - expect(cursor4.getBufferPosition()).toEqual([3,1]) + expect(cursor1.getBufferPosition()).toEqual([0, 1]) + expect(cursor2.getBufferPosition()).toEqual([1, 4]) + expect(cursor3.getBufferPosition()).toEqual([2, 3]) + expect(cursor4.getBufferPosition()).toEqual([3, 1]) editor.deleteToBeginningOfSubword() expect(buffer.lineForRow(0)).toBe('') expect(buffer.lineForRow(1)).toBe(' viousWord') expect(buffer.lineForRow(2)).toBe('e ') expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0,0]) - expect(cursor2.getBufferPosition()).toEqual([1,1]) - expect(cursor3.getBufferPosition()).toEqual([2,1]) - expect(cursor4.getBufferPosition()).toEqual([3,0]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 1]) + expect(cursor3.getBufferPosition()).toEqual([2, 1]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) editor.deleteToBeginningOfSubword() expect(buffer.lineForRow(0)).toBe('') expect(buffer.lineForRow(1)).toBe('viousWord') expect(buffer.lineForRow(2)).toBe(' ') expect(buffer.lineForRow(3)).toBe('') - expect(cursor1.getBufferPosition()).toEqual([0,0]) - expect(cursor2.getBufferPosition()).toEqual([1,0]) - expect(cursor3.getBufferPosition()).toEqual([2,0]) - expect(cursor4.getBufferPosition()).toEqual([2,1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([2, 1]) describe ".deleteToEndOfSubword", -> it "deletes subwords", -> @@ -4611,10 +4611,10 @@ describe "TextEditor", -> editor.insertText("getPreviousWord \n") editor.insertText("e, => \n") editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0,0]) - editor.addCursorAtBufferPosition([1,0]) - editor.addCursorAtBufferPosition([2,2]) - editor.addCursorAtBufferPosition([3,0]) + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 0]) [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() editor.deleteToEndOfSubword() @@ -4622,20 +4622,20 @@ describe "TextEditor", -> expect(buffer.lineForRow(1)).toBe('PreviousWord ') expect(buffer.lineForRow(2)).toBe('e, ') expect(buffer.lineForRow(3)).toBe('88 ') - expect(cursor1.getBufferPosition()).toEqual([0,0]) - expect(cursor2.getBufferPosition()).toEqual([1,0]) - expect(cursor3.getBufferPosition()).toEqual([2,2]) - expect(cursor4.getBufferPosition()).toEqual([3,0]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) editor.deleteToEndOfSubword() expect(buffer.lineForRow(0)).toBe('') expect(buffer.lineForRow(1)).toBe('Word ') expect(buffer.lineForRow(2)).toBe('e,') expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0,0]) - expect(cursor2.getBufferPosition()).toEqual([1,0]) - expect(cursor3.getBufferPosition()).toEqual([2,2]) - expect(cursor4.getBufferPosition()).toEqual([3,0]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) describe 'gutters', -> describe 'the TextEditor constructor', -> From 5386bf90988d24b16a40264a3f9f1190863de1b3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Jul 2015 12:27:40 -0500 Subject: [PATCH 6/6] Add emacs-style subword movement bindings --- keymaps/emacs.cson | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/keymaps/emacs.cson b/keymaps/emacs.cson index 323773e68..764c97938 100644 --- a/keymaps/emacs.cson +++ b/keymaps/emacs.cson @@ -1,7 +1,13 @@ 'atom-text-editor': 'alt-f': 'editor:move-to-end-of-word' + 'alt-ctrl-f': 'editor:move-to-next-subword-boundary' 'alt-F': 'editor:select-to-end-of-word' + 'alt-ctrl-F': 'editor:select-to-next-subword-boundary' 'alt-b': 'editor:move-to-beginning-of-word' + 'alt-ctrl-b': 'editor:move-to-previous-subword-boundary' 'alt-B': 'editor:select-to-beginning-of-word' + 'alt-ctrl-B': 'editor:select-to-previous-subword-boundary' 'alt-h': 'editor:delete-to-beginning-of-word' + 'alt-ctrl-h': 'editor:delete-to-beginning-of-subword' 'alt-d': 'editor:delete-to-end-of-word' + 'alt-ctrl-d': 'editor:delete-to-end-of-subword'