diff --git a/lib/ace/keyboard/vim.js b/lib/ace/keyboard/vim.js index 0641897f..c709b67e 100644 --- a/lib/ace/keyboard/vim.js +++ b/lib/ace/keyboard/vim.js @@ -1135,9 +1135,11 @@ dom.importCssString(".normal-mode .ace_cursor{\ } var numberRegex = /[\d]/; - var wordRegexp = [{test: CodeMirror.isWordChar}, {test: function(ch) { - return !CodeMirror.isWordChar(ch) && !/\s/.test(ch); - }}], bigWordRegexp = [(/\S/)]; + var wordCharTest = [CodeMirror.isWordChar, function(ch) { + return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); + }], bigWordCharTest = [function(ch) { + return /\S/.test(ch); + }]; function makeKeyRange(start, size) { var keys = []; for (var i = start; i < start + size; i++) { @@ -1417,6 +1419,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Testing hook. maybeInitVimState_: maybeInitVimState, + suppressErrorLogging: false, + InsertModeKey: InsertModeKey, map: function(lhs, rhs, ctx) { // Add user defined key bindings. @@ -1567,7 +1571,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ // clear VIM state in case it's in a bad state. cm.state.vim = undefined; maybeInitVimState(cm); - console['log'](e); + if (!CodeMirror.Vim.suppressErrorLogging) { + console['log'](e); + } throw e; } return true; @@ -1577,7 +1583,16 @@ dom.importCssString(".normal-mode .ace_cursor{\ }, handleEx: function(cm, input) { exCommandDispatcher.processCommand(cm, input); - } + }, + + defineMotion: defineMotion, + defineAction: defineAction, + defineOperator: defineOperator, + mapCommand: mapCommand, + _mapCommand: _mapCommand, + + exitVisualMode: exitVisualMode, + exitInsertMode: exitInsertMode }; // Represents the current input state. @@ -1827,12 +1842,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ break; case 'search': this.processSearch(cm, vim, command); - clearInputState(cm); break; case 'ex': case 'keyToEx': this.processEx(cm, vim, command); - clearInputState(cm); break; default: break; @@ -1925,6 +1938,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ updateSearchQuery(cm, query, ignoreCase, smartCase); } catch (e) { showConfirm(cm, 'Invalid regex: ' + query); + clearInputState(cm); return; } commandDispatcher.processMotion(cm, vim, { @@ -1974,6 +1988,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ clearSearchHighlight(cm); cm.scrollTo(originalScrollPos.left, originalScrollPos.top); CodeMirror.e_stop(e); + clearInputState(cm); close(); cm.focus(); } @@ -2040,6 +2055,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.reset(); CodeMirror.e_stop(e); + clearInputState(cm); close(); cm.focus(); } @@ -2250,7 +2266,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ var operatorMoveTo = operators[operator]( cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); if (vim.visualMode) { - exitVisualMode(cm); + exitVisualMode(cm, operatorMoveTo != null); } if (operatorMoveTo) { cm.setCursor(operatorMoveTo); @@ -2627,6 +2643,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ } }; + function defineMotion(name, fn) { + motions[name] = fn; + } + function fillArray(val, times) { var arr = []; for (var i = 0; i < times; i++) { @@ -2709,7 +2729,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ vimGlobalState.registerController.pushText( args.registerName, 'delete', text, args.linewise, vim.visualBlock); - return finalHead; + return clipCursorToContent(cm, finalHead); }, indent: function(cm, args, ranges) { var vim = cm.state.vim; @@ -2777,6 +2797,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ } }; + function defineOperator(name, fn) { + operators[name] = fn; + } + var actions = { jumpListWalk: function(cm, actionArgs, vim) { if (vim.visualMode) { @@ -2993,6 +3017,11 @@ dom.importCssString(".normal-mode .ace_cursor{\ if (vim.visualMode) { curStart = cm.getCursor('anchor'); curEnd = cm.getCursor('head'); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curEnd; + curEnd = curStart; + curStart = tmp; + } curEnd.ch = lineLength(cm, curEnd.line) - 1; } else { // Repeat is the number of lines to join. Minimum 2 lines. @@ -3011,10 +3040,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.replaceRange(text, curStart, tmp); } var curFinalPos = Pos(curStart.line, finalCh); - cm.setCursor(curFinalPos); if (vim.visualMode) { - exitVisualMode(cm); + exitVisualMode(cm, false); } + cm.setCursor(curFinalPos); }, newLineAndEnterInsertMode: function(cm, actionArgs, vim) { vim.insertMode = true; @@ -3177,7 +3206,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ } } if (vim.visualMode) { - exitVisualMode(cm); + exitVisualMode(cm, false); } cm.setCursor(curPosFinal); }, @@ -3235,7 +3264,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? selections[0].anchor : selections[0].head; cm.setCursor(curStart); - exitVisualMode(cm); + exitVisualMode(cm, false); } else { cm.setCursor(offsetCursor(curEnd, 0, -1)); } @@ -3283,6 +3312,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ exitInsertMode: exitInsertMode }; + function defineAction(name, fn) { + actions[name] = fn; + } + /* * Below are miscellaneous utility functions used by vim.js */ @@ -3412,9 +3445,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ function lineLength(cm, lineNum) { return cm.getLine(lineNum).length; } - function reverse(s){ - return s.split('').reverse().join(''); - } function trim(s) { if (s.trim) { return s.trim(); @@ -3728,59 +3758,38 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Seek to first word or non-whitespace character, depending on if // noSymbol is true. - var textAfterIdx = line.substring(idx); - var firstMatchedChar; - if (noSymbol) { - firstMatchedChar = textAfterIdx.search(/\w/); - } else { - firstMatchedChar = textAfterIdx.search(/\S/); + var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; + while (!test(line.charAt(idx))) { + idx++; + if (idx >= line.length) { return null; } } - if (firstMatchedChar == -1) { - return null; - } - idx += firstMatchedChar; - textAfterIdx = line.substring(idx); - var textBeforeIdx = line.substring(0, idx); - var matchRegex; - // Greedy matchers for the "word" we are trying to expand. if (bigWord) { - matchRegex = /^\S+/; + test = bigWordCharTest[0]; } else { - if ((/\w/).test(line.charAt(idx))) { - matchRegex = /^\w+/; - } else { - matchRegex = /^[^\w\s]+/; + test = wordCharTest[0]; + if (!test(line.charAt(idx))) { + test = wordCharTest[1]; } } - var wordAfterRegex = matchRegex.exec(textAfterIdx); - var wordStart = idx; - var wordEnd = idx + wordAfterRegex[0].length; - // TODO: Find a better way to do this. It will be slow on very long lines. - var revTextBeforeIdx = reverse(textBeforeIdx); - var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); - if (wordBeforeRegex) { - wordStart -= wordBeforeRegex[0].length; - } + var end = idx, start = idx; + while (test(line.charAt(end)) && end < line.length) { end++; } + while (test(line.charAt(start)) && start >= 0) { start--; } + start++; if (inclusive) { - // If present, trim all whitespace after word. - // Otherwise, trim all whitespace before word. - var textAfterWordEnd = line.substring(wordEnd); - var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; - if (whitespacesAfterWord > 0) { - wordEnd += whitespacesAfterWord; - } else { - var revTrim = revTextBeforeIdx.length - wordStart; - var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); - var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; - wordStart -= whitespacesBeforeWord; + // If present, include all whitespace after word. + // Otherwise, include all whitespace before word, except indentation. + var wordEnd = end; + while (/\s/.test(line.charAt(end)) && end < line.length) { end++; } + if (wordEnd == end) { + var wordStart = start; + while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } + if (!start) { start = wordStart; } } } - - return { start: Pos(cur.line, wordStart), - end: Pos(cur.line, wordEnd) }; + return { start: Pos(cur.line, start), end: Pos(cur.line, end) }; } function recordJumpPosition(cm, oldCur, newCur) { @@ -3938,7 +3947,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ var pos = cur.ch; var line = cm.getLine(lineNum); var dir = forward ? 1 : -1; - var regexps = bigWord ? bigWordRegexp : wordRegexp; + var charTests = bigWord ? bigWordCharTest: wordCharTest; if (emptyLineIsWord && line == '') { lineNum += dir; @@ -3958,11 +3967,11 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Find bounds of next word. while (pos != stop) { var foundWord = false; - for (var i = 0; i < regexps.length && !foundWord; ++i) { - if (regexps[i].test(line.charAt(pos))) { + for (var i = 0; i < charTests.length && !foundWord; ++i) { + if (charTests[i](line.charAt(pos))) { wordStart = pos; // Advance to end of word. - while (pos != stop && regexps[i].test(line.charAt(pos))) { + while (pos != stop && charTests[i](line.charAt(pos))) { pos += dir; } wordEnd = pos; @@ -4279,6 +4288,12 @@ dom.importCssString(".normal-mode .ace_cursor{\ }, setReversed: function(reversed) { vimGlobalState.isReversed = reversed; + }, + getScrollbarAnnotate: function() { + return this.annotate; + }, + setScrollbarAnnotate: function(annotate) { + this.annotate = annotate; } }; function getSearchState(cm) { @@ -4557,14 +4572,21 @@ dom.importCssString(".normal-mode .ace_cursor{\ }; } function highlightSearchMatches(cm, query) { - var overlay = getSearchState(cm).getOverlay(); + var searchState = getSearchState(cm); + var overlay = searchState.getOverlay(); if (!overlay || query != overlay.query) { if (overlay) { cm.removeOverlay(overlay); } overlay = searchOverlay(query); cm.addOverlay(overlay); - getSearchState(cm).setOverlay(overlay); + if (cm.showMatchesOnScrollbar) { + if (searchState.getScrollbarAnnotate()) { + searchState.getScrollbarAnnotate().clear(); + } + searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); + } + searchState.setOverlay(overlay); } } function findNext(cm, prev, query, repeat) { @@ -4589,8 +4611,13 @@ dom.importCssString(".normal-mode .ace_cursor{\ }); } function clearSearchHighlight(cm) { + var state = getSearchState(cm); cm.removeOverlay(getSearchState(cm).getOverlay()); - getSearchState(cm).setOverlay(null); + state.setOverlay(null); + if (state.getScrollbarAnnotate()) { + state.getScrollbarAnnotate().clear(); + state.setScrollbarAnnotate(null); + } } /** * Check if pos is in the specified range, INCLUSIVE. @@ -5417,6 +5444,19 @@ dom.importCssString(".normal-mode .ace_cursor{\ } } + function _mapCommand(command) { + defaultKeymap.push(command); + } + + function mapCommand(keys, type, name, args, extra) { + var command = {keys: keys, type: type}; + command[type] = name; + command[type + "Args"] = args; + for (var key in extra) + command[key] = extra[key]; + _mapCommand(command); + } + // The timeout in milliseconds for the two-character ESC keymap should be // adjusted according to your typing speed to prevent false positives. defineOption('insertModeEscKeysTimeout', 200, 'number'); @@ -5597,6 +5637,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ var macroModeState = vimGlobalState.macroModeState; var lastChange = macroModeState.lastInsertModeChanges; var keyName = CodeMirror.keyName(e); + if (!keyName) { return; } function onKeyFound() { lastChange.changes.push(new InsertModeKey(keyName)); return true; diff --git a/lib/ace/keyboard/vim_test.js b/lib/ace/keyboard/vim_test.js index 8b726a33..788cfccb 100644 --- a/lib/ace/keyboard/vim_test.js +++ b/lib/ace/keyboard/vim_test.js @@ -73,6 +73,9 @@ function test(name, fn) { } vim.CodeMirror.Vim.unmap("Y"); +vim.CodeMirror.Vim.defineEx('write', 'w', function(cm) { + CodeMirror.commands.save(cm); +}); @@ -685,7 +688,7 @@ testVim('dl_eol', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq(' ', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 6); + helpers.assertCursorAt(0, 5); }, { value: ' word1 ' }); testVim('dl_repeat', function(cm, vim, helpers) { var curStart = makeCursor(0, 0); @@ -767,6 +770,16 @@ testVim('dw_word', function(cm, vim, helpers) { is(!register.linewise); eqPos(curStart, cm.getCursor()); }, { value: ' word1 word2' }); +testVim('dw_unicode_word', function(cm, vim, helpers) { + helpers.doKeys('d', 'w'); + eq(cm.getValue().length, 10); + helpers.doKeys('d', 'w'); + eq(cm.getValue().length, 6); + helpers.doKeys('d', 'w'); + eq(cm.getValue().length, 5); + helpers.doKeys('d', 'e'); + eq(cm.getValue().length, 2); +}, { value: ' \u0562\u0561\u0580\u0587\xbbe\xb5g ' }); testVim('dw_only_word', function(cm, vim, helpers) { // Test that if there is only 1 word left, dw deletes till the end of the // line. @@ -776,7 +789,7 @@ testVim('dw_only_word', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('word1 ', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 1); + helpers.assertCursorAt(0, 0); }, { value: ' word1 ' }); testVim('dw_eol', function(cm, vim, helpers) { // Assert that dw does not delete the newline if last word to delete is at end @@ -787,7 +800,7 @@ testVim('dw_eol', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('word1', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 1); + helpers.assertCursorAt(0, 0); }, { value: ' word1\nword2' }); testVim('dw_eol_with_multiple_newlines', function(cm, vim, helpers) { // Assert that dw does not delete the newline if last word to delete is at end @@ -798,7 +811,7 @@ testVim('dw_eol_with_multiple_newlines', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('word1', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 1); + helpers.assertCursorAt(0, 0); }, { value: ' word1\n\nword2' }); testVim('dw_empty_line_followed_by_whitespace', function(cm, vim, helpers) { cm.setCursor(0, 0); @@ -844,7 +857,7 @@ testVim('dw_repeat', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('word1\nword2', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 1); + helpers.assertCursorAt(0, 0); }, { value: ' word1\nword2' }); testVim('de_word_start_and_empty_lines', function(cm, vim, helpers) { cm.setCursor(0, 0); @@ -1371,7 +1384,7 @@ testVim('D', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('rd1', register.toString()); is(!register.linewise); - helpers.assertCursorAt(0, 3); + helpers.assertCursorAt(0, 2); }, { value: ' word1\nword2\n word3' }); testVim('C', function(cm, vim, helpers) { var curStart = makeCursor(0, 3); @@ -1962,7 +1975,6 @@ testVim('visual_block_move_to_eol', function(cm, vim, helpers) { cm.setCursor(0, 0); helpers.doKeys('', 'G', '$'); var selections = cm.getSelections().join(); - console.log(selections); eq("123,45,6", selections); }, {value: '123\n45\n6'}); testVim('visual_block_different_line_lengths', function(cm, vim, helpers) { @@ -2053,6 +2065,11 @@ testVim('visual_join', function(cm, vim, helpers) { eq(' 1 2 3\n 4\n 5', cm.getValue()); is(!vim.visualMode); }, { value: ' 1\n 2\n 3\n 4\n 5' }); +testVim('visual_join_2', function(cm, vim, helpers) { + helpers.doKeys('G', 'V', 'g', 'g', 'J'); + eq('1 2 3 4 5 6 ', cm.getValue()); + is(!vim.visualMode); +}, { value: '1\n2\n3\n4\n5\n6\n'}); testVim('visual_blank', function(cm, vim, helpers) { helpers.doKeys('v', 'k'); eq(vim.visualMode, true); @@ -2260,6 +2277,15 @@ testVim('S_visual', function(cm, vim, helpers) { eq('\ncc', cm.getValue()); }, { value: 'aa\nbb\ncc'}); +testVim('d_/', function(cm, vim, helpers) { + cm.openDialog = helpers.fakeOpenDialog('match'); + helpers.doKeys('2', 'd', '/'); + helpers.assertCursorAt(0, 0); + eq('match \n next', cm.getValue()); + cm.openDialog = helpers.fakeOpenDialog('2'); + helpers.doKeys('d', ':'); + // TODO eq(' next', cm.getValue()); +}, { value: 'text match match \n next' }); testVim('/ and n/N', function(cm, vim, helpers) { cm.openDialog = helpers.fakeOpenDialog('match'); helpers.doKeys('/'); @@ -3204,7 +3230,7 @@ testVim('scrollMotion', function(cm, vim, helpers){ cm.refresh(); //ace! prevScrollInfo = cm.getScrollInfo(); helpers.doKeys(''); - eq(prevCursor.line - 1, cm.getCursor().line); + eq(prevCursor.line - 1, cm.getCursor().line, "Y"); is(prevScrollInfo.top > cm.getScrollInfo().top); }, { value: scrollMotionSandbox});