diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 34d18e159..8180f65bb 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/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' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index cd71d99e7..9c1bbd4a1 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 5d3629386..2a89962f3 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -99,6 +99,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/package.json b/package.json index 1cb5940a7..c0dd5ffde 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 64bb7adba..a2ade3753 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4364,6 +4364,279 @@ describe "TextEditor", -> waitsForPromise -> editor.checkoutHeadRevision() + describe ".moveToPreviousSubwordBoundary", -> + it "does not move the cursor when there is no previous subword boundary", -> + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "stops at word and underscore boundaries", -> + 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 "stops at camelCase boundaries", -> + 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 "skips 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 "skips 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 "skips 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]) + + it "works with multiple cursors", -> + 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 move the cursor when there is no next subword boundary", -> + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "stops at word and underscore boundaries", -> + 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 "stops at camelCase boundaries", -> + 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 "skips 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 "skips 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 "skips 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]) + + 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]) + + 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('') + 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]) + describe 'gutters', -> describe 'the TextEditor constructor', -> it 'creates a line-number gutter', -> diff --git a/src/cursor.coffee b/src/cursor.coffee index 0681e51ca..61f12cc8c 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -399,6 +399,18 @@ 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) + @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: -> @@ -433,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}) -> @@ -636,6 +648,29 @@ 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 ]+", + "[\t ]+$", + "[A-Z]?[a-z]+", + "[A-Z]+(?![a-z])", + "\\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 6a4d4726f..a3cb4e0d3 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -290,6 +290,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: -> @@ -458,6 +466,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 e3eaaeb2a..4c63f4ec0 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -262,6 +262,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() @@ -271,6 +273,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() ) @@ -287,6 +291,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 37d9c2021..3b8653e3f 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1078,6 +1078,18 @@ class TextEditor extends Model deleteToNextWordBoundary: -> @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() + # 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. @@ -1703,6 +1715,14 @@ class TextEditor extends Model moveToNextWordBoundary: -> @moveCursors (cursor) -> cursor.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() @@ -2021,6 +2041,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.