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.