diff --git a/keymaps/vim-mode.cson b/keymaps/vim-mode.cson index 01543dee..3fad54f9 100644 --- a/keymaps/vim-mode.cson +++ b/keymaps/vim-mode.cson @@ -85,10 +85,12 @@ 'down': 'vim-mode:move-down' 'w': 'vim-mode:move-to-next-word' + 'alt-w': 'vim-mode:move-to-next-alt-word' 'W': 'vim-mode:move-to-next-whole-word' 'e': 'vim-mode:move-to-end-of-word' 'E': 'vim-mode:move-to-end-of-whole-word' 'b': 'vim-mode:move-to-previous-word' + 'alt-b': 'vim-mode:move-to-previous-alt-word' 'B': 'vim-mode:move-to-previous-whole-word' '}': 'vim-mode:move-to-next-paragraph' '{': 'vim-mode:move-to-previous-paragraph' diff --git a/lib/motions/general-motions.coffee b/lib/motions/general-motions.coffee index 47a3cc78..435e5f86 100644 --- a/lib/motions/general-motions.coffee +++ b/lib/motions/general-motions.coffee @@ -183,9 +183,27 @@ class MoveToPreviousWord extends Motion operatesInclusively: false moveCursor: (cursor, count=1) -> + if settings.defaultWordIsCamelCaseSensitive() + @moveCursorToPreviousSubword(cursor, count) + else + @moveCursorToPreviousWord(cursor, count) + + moveCursorToPreviousSubword: (cursor, count) -> + _.times count, -> + # XXX: Doesn't actually work as it also moves to the ending of the previous subword + cursor.moveToPreviousSubwordBoundary() + + moveCursorToPreviousWord: (cursor, count) -> _.times count, -> cursor.moveToBeginningOfWord() +class MoveToPreviousAltWord extends MoveToPreviousWord + moveCursor: (cursor, count=1) -> + if settings.defaultWordIsCamelCaseSensitive() + @moveCursorToPreviousWord(cursor, count) + else + @moveCursorToPreviousSubword(cursor, count) + class MoveToPreviousWholeWord extends Motion operatesInclusively: false @@ -204,17 +222,31 @@ class MoveToPreviousWholeWord extends Motion not cur.row and not cur.column class MoveToNextWord extends Motion - wordRegex: null + subwordRegex: null operatesInclusively: false moveCursor: (cursor, count=1, options) -> + if settings.defaultWordIsCamelCaseSensitive() + @moveCursorToNextSubword(cursor, count, options) + else + @moveCursorToNextWord(cursor, count, options) + + moveCursorToNextWord: (cursor, count, options) -> + @moveCursorByRegex(cursor, count, options, null) + + moveCursorToNextSubword: (cursor, count, options) -> + @subwordRegex ?= cursor.subwordRegExp() + # HACK: expected behavior got changed with https://github.com/atom/atom/commit/ba3ab41 + @moveCursorByRegex(cursor, count, options, new RegExp("^[\t ]*$|"+@subwordRegex.source.substring(14), "g")) + + moveCursorByRegex: (cursor, count, options, wordRegex) -> _.times count, => current = cursor.getBufferPosition() next = if options?.excludeWhitespace - cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex) + cursor.getEndOfCurrentWordBufferPosition(wordRegex: wordRegex) else - cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex) + cursor.getBeginningOfNextWordBufferPosition(wordRegex: wordRegex) return if @isEndOfFile(cursor) @@ -232,8 +264,16 @@ class MoveToNextWord extends Motion eof = @editor.getEofBufferPosition() cur.row is eof.row and cur.column is eof.column +class MoveToNextAltWord extends MoveToNextWord + moveCursor: (cursor, count=1, options) -> + if settings.defaultWordIsCamelCaseSensitive() + @moveCursorToNextWord(cursor, count, options) + else + @moveCursorToNextSubword(cursor, count, options) + class MoveToNextWholeWord extends MoveToNextWord - wordRegex: WholeWordOrEmptyLineRegex + moveCursor: (cursor, count=1, options) -> + @moveCursorByRegex(cursor, count, options, WholeWordOrEmptyLineRegex) class MoveToEndOfWord extends Motion wordRegex: null @@ -455,7 +495,7 @@ class ScrollFullDownKeepCursor extends ScrollKeepingCursor module.exports = { Motion, MotionWithInput, CurrentSelection, MoveLeft, MoveRight, MoveUp, MoveDown, - MoveToPreviousWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextWholeWord, + MoveToPreviousWord, MoveToPreviousAltWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextAltWord, MoveToNextWholeWord, MoveToEndOfWord, MoveToNextParagraph, MoveToPreviousParagraph, MoveToAbsoluteLine, MoveToRelativeLine, MoveToBeginningOfLine, MoveToFirstCharacterOfLineUp, MoveToFirstCharacterOfLineDown, MoveToFirstCharacterOfLine, MoveToFirstCharacterOfLineAndDown, MoveToLastCharacterOfLine, diff --git a/lib/settings.coffee b/lib/settings.coffee index 30ff35b9..2ab4c807 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -17,6 +17,10 @@ settings = type: 'string' default: '-?[0-9]+' description: 'Use this to control how Ctrl-A/Ctrl-X finds numbers; use "(?:\\B-)?[0-9]+" to treat numbers as positive if the minus is preceded by a character, e.g. in "identifier-1".' + defaultWordIsCamelCaseSensitive: + type: 'boolean' + default: true + description: 'If set to true, the w and similar motions and text objects will be camel-case sensitive. The alt modifier key can be used to access the camel-case insensitive behaviour. Setting to false reverses the bindings.' Object.keys(settings.config).forEach (k) -> settings[k] = -> diff --git a/lib/vim-state.coffee b/lib/vim-state.coffee index c5c432ef..d8adeee8 100644 --- a/lib/vim-state.coffee +++ b/lib/vim-state.coffee @@ -105,10 +105,12 @@ class VimState 'move-down': => new Motions.MoveDown(@editor, this) 'move-right': => new Motions.MoveRight(@editor, this) 'move-to-next-word': => new Motions.MoveToNextWord(@editor, this) + 'move-to-next-alt-word': => new Motions.MoveToNextAltWord(@editor, this) 'move-to-next-whole-word': => new Motions.MoveToNextWholeWord(@editor, this) 'move-to-end-of-word': => new Motions.MoveToEndOfWord(@editor, this) 'move-to-end-of-whole-word': => new Motions.MoveToEndOfWholeWord(@editor, this) 'move-to-previous-word': => new Motions.MoveToPreviousWord(@editor, this) + 'move-to-previous-alt-word': => new Motions.MoveToPreviousAltWord(@editor, this) 'move-to-previous-whole-word': => new Motions.MoveToPreviousWholeWord(@editor, this) 'move-to-next-paragraph': => new Motions.MoveToNextParagraph(@editor, this) 'move-to-previous-paragraph': => new Motions.MoveToPreviousParagraph(@editor, this) diff --git a/spec/motions-spec.coffee b/spec/motions-spec.coffee index f7114edd..70cd6ca3 100644 --- a/spec/motions-spec.coffee +++ b/spec/motions-spec.coffee @@ -125,68 +125,153 @@ describe "Motions", -> keydown('l') expect(editor.getCursorBufferPosition()).toEqual [1, 0] - describe "the w keybinding", -> - beforeEach -> editor.setText("ab cde1+- \n xyz\n\nzip") + describe "the w and alt-w keybindings", -> + itMovesByWord = (key, options) -> + describe "moving by word", -> + beforeEach -> editor.setText("ab = cDeFg1+- \n xyz\n\nzip") - describe "as a motion", -> - beforeEach -> editor.setCursorScreenPosition([0, 0]) + describe "as a motion", -> + beforeEach -> editor.setCursorScreenPosition([0, 0]) - it "moves the cursor to the beginning of the next word", -> - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [0, 3] + it "moves the cursor to the beginning of the next word", -> + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 3] - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [0, 7] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 5] - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [1, 1] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 11] - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [2, 0] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [1, 1] - # FIXME: The definition of Cursor#getEndOfCurrentWordBufferPosition, - # means that the end of the word can't be the current cursor - # position (even though it is when your cursor is on a new line). - # - # Therefore it picks the end of the next word here (which is [3,3]) - # to start looking for the next word, which is also the end of the - # buffer so the cursor never advances. - # - # See atom/vim-mode#3 - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [3, 0] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [2, 0] - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [3, 3] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 0] - # After cursor gets to the EOF, it should stay there. - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual [3, 3] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 3] - it "moves the cursor to the end of the word if last word in file", -> - editor.setText("abc") - editor.setCursorScreenPosition([0, 0]) - keydown('w') - expect(editor.getCursorScreenPosition()).toEqual([0, 3]) + # After cursor gets to the EOF, it should stay there. + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 3] - describe "as a selection", -> - describe "within a word", -> - beforeEach -> - editor.setCursorScreenPosition([0, 0]) - keydown('y') - keydown('w') + it "moves the cursor to the end of the word if last word in file", -> + editor.setText("abc") + editor.setCursorScreenPosition([0, 0]) + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual([0, 3]) - it "selects to the end of the word", -> - expect(vimState.getRegister('"').text).toBe 'ab ' + describe "as a selection", -> + describe "within a word", -> + beforeEach -> + editor.setCursorScreenPosition([0, 5]) + keydown('y') + keydown(key, options) - describe "between words", -> - beforeEach -> - editor.setCursorScreenPosition([0, 2]) - keydown('y') - keydown('w') + it "selects to the beginning of the next word", -> + expect(vimState.getRegister('"').text).toBe 'cDeFg1' + + describe "between words", -> + beforeEach -> + editor.setCursorScreenPosition([0, 2]) + keydown('y') + keydown(key, options) + + it "selects the whitespace", -> + expect(vimState.getRegister('"').text).toBe ' ' + + itMovesBySubword = (key, options) -> + describe "moving by subword", -> + beforeEach -> editor.setText("ab = cDeFg1+- \n xyz\n\nzip") + + describe "as a motion", -> + beforeEach -> editor.setCursorScreenPosition([0, 0]) + + it "moves the cursor to the beginning of the next subword", -> + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 3] - it "selects the whitespace", -> - expect(vimState.getRegister('"').text).toBe ' ' + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 5] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 6] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 8] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 10] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 11] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [1, 1] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [2, 0] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 0] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 3] + + # After cursor gets to the EOF, it should stay there. + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 3] + + it "moves the cursor to the end of the word if last word in file", -> + editor.setText("abc") + editor.setCursorScreenPosition([0, 0]) + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual([0, 3]) + + describe "as a selection", -> + describe "within a word", -> + beforeEach -> + editor.setCursorScreenPosition([0, 5]) + keydown('y') + keydown(key, options) + + it "selects to the beginning of the next word", -> + expect(vimState.getRegister('"').text).toBe 'c' + + describe "between words", -> + beforeEach -> + editor.setCursorScreenPosition([0, 4]) + keydown('y') + keydown(key, options) + + it "selects the whitespace", -> + expect(vimState.getRegister('"').text).toBe ' ' + + describe "the w keybinding", -> + describe "with it configured to be camel-case insensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', false) + + itMovesByWord('w') + + describe "with it configured to be camel-case sensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', true) + + itMovesBySubword('w') + + describe "the alt-w keybinding", -> + describe "with it configured to be camel-case insensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', true) + + itMovesByWord('w', alt: true) + + describe "with it configured to be camel-case sensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', false) + + itMovesBySubword('w', alt: true) describe "the W keybinding", -> beforeEach -> editor.setText("cde1+- ab \n xyz\n\nzip") @@ -379,62 +464,159 @@ describe "Motions", -> it 'selects to the beginning of the current paragraph', -> expect(vimState.getRegister('"').text).toBe "\nzip\n" - describe "the b keybinding", -> - beforeEach -> editor.setText(" ab cde1+- \n xyz\n\nzip }\n last") + describe "the b and alt-b keybindings", -> + itMovesByWord = (key, options) -> + describe "moving by word", -> + beforeEach -> editor.setText(" ab = cDeFg1+- \n xyz\n\nzip }\n last") - describe "as a motion", -> - beforeEach -> editor.setCursorScreenPosition([4, 1]) + describe "as a motion", -> + beforeEach -> editor.setCursorScreenPosition([4, 1]) - it "moves the cursor to the beginning of the previous word", -> - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [3, 4] + it "moves the cursor to the beginning of the previous word", -> + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 4] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [3, 0] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 0] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [2, 0] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [2, 0] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [1, 1] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [1, 1] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [0, 8] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 12] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [0, 4] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 6] - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [0, 1] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 4] - # Go to start of the file, after moving past the first word - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [0, 0] + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 1] - # Stay at the start of the file - keydown('b') - expect(editor.getCursorScreenPosition()).toEqual [0, 0] + # Go to start of the file, after moving past the first word + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] - describe "as a selection", -> - describe "within a word", -> - beforeEach -> - editor.setCursorScreenPosition([0, 2]) - keydown('y') - keydown('b') + # Stay at the start of the file + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] - it "selects to the beginning of the current word", -> - expect(vimState.getRegister('"').text).toBe 'a' - expect(editor.getCursorScreenPosition()).toEqual [0, 1] + describe "as a selection", -> + describe "within a word", -> + beforeEach -> + editor.setCursorScreenPosition([0, 10]) + keydown('y') + keydown(key, options) - describe "between words", -> - beforeEach -> - editor.setCursorScreenPosition([0, 4]) - keydown('y') - keydown('b') + it "selects to the beginning of the current word", -> + expect(vimState.getRegister('"').text).toBe "cDeF" + expect(editor.getCursorScreenPosition()).toEqual [0, 6] - it "selects to the beginning of the last word", -> - expect(vimState.getRegister('"').text).toBe 'ab ' - expect(editor.getCursorScreenPosition()).toEqual [0, 1] + describe "between words", -> + beforeEach -> + editor.setCursorScreenPosition([0, 4]) + keydown('y') + keydown(key, options) + + it "selects to the beginning of the last word", -> + expect(vimState.getRegister('"').text).toBe 'ab ' + expect(editor.getCursorScreenPosition()).toEqual [0, 1] + + itMovesBySubword = (key, options) -> + describe "moving by subword", -> + beforeEach -> editor.setText(" ab = cDeFg1+- \n xyz\n\nzip }\n last") + + describe "as a motion", -> + beforeEach -> editor.setCursorScreenPosition([4, 1]) + + it "moves the cursor to the beginning of the previous word", -> + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 4] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [3, 0] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [2, 0] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [1, 1] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 12] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 11] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 9] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 7] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 6] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 4] + + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 1] + + # Go to start of the file, after moving past the first word + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + + # Stay at the start of the file + keydown(key, options) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + + describe "as a selection", -> + describe "within a word", -> + beforeEach -> + editor.setCursorScreenPosition([0, 10]) + keydown('y') + keydown(key, options) + + it "selects to the beginning of the current word", -> + expect(vimState.getRegister('"').text).toBe "F" + expect(editor.getCursorScreenPosition()).toEqual [0, 9] + + describe "between words", -> + beforeEach -> + editor.setCursorScreenPosition([0, 4]) + keydown('y') + keydown(key, options) + + it "selects to the beginning of the last word", -> + expect(vimState.getRegister('"').text).toBe 'ab ' + expect(editor.getCursorScreenPosition()).toEqual [0, 1] + + describe "the b keybinding", -> + describe "with it configured to be camel-case insensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', false) + + itMovesByWord('b') + + describe "with it configured to be camel-case sensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', true) + + itMovesBySubword('b') + + describe "the alt-b keybinding", -> + describe "with it configured to be camel-case insensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', true) + + itMovesByWord('b', alt: true) + + describe "with it configured to be camel-case sensitive", -> + beforeEach -> atom.config.set('vim-mode.defaultWordIsCamelCaseSensitive', false) + + itMovesBySubword('b', alt: true) describe "the B keybinding", -> beforeEach -> editor.setText("cde1+- ab \n\t xyz-123\n\n zip")