diff --git a/src/keyboard/vim.js b/src/keyboard/vim.js index 5360c8d55a0..328c661ad4c 100644 --- a/src/keyboard/vim.js +++ b/src/keyboard/vim.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/5/LICENSE /** * Supported keybindings: @@ -62,7 +62,7 @@ } var Range = require("../range").Range; var EventEmitter = require("../lib/event_emitter").EventEmitter; - var dom = require("../lib/dom"); + var domLib = require("../lib/dom"); var oop = require("../lib/oop"); var KEYS = require("../lib/keys"); var event = require("../lib/event"); @@ -740,7 +740,7 @@ CodeMirror.defineExtension = function(name, fn) { CodeMirror.prototype[name] = fn; }; -dom.importCssString(`.normal-mode .ace_cursor{ +domLib.importCssString(`.normal-mode .ace_cursor{ border: none; background-color: rgba(255,0,0,0.5); } @@ -945,7 +945,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ { keys: '', type: 'keyToKey', toKeys: '' }, { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' }, { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' }, - { keys: '', type: 'keyToKey', toKeys: '' }, // ace_patch ipad keyboard sends C-Esc instead of C-[ + { keys: '', type: 'keyToKey', toKeys: '' }, // ipad keyboard sends C-Esc instead of C-[ { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' }, { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' }, { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'}, @@ -1041,7 +1041,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'}, { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'}, - // { keys: '', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' }, + { keys: '', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' }, { keys: '', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' }, //ignore C-w in normal mode { keys: '', type: 'idle', context: 'normal' }, @@ -1085,8 +1085,8 @@ dom.importCssString(`.normal-mode .ace_cursor{ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }}, { keys: 'z', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, - { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }}, - { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }}, + { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: '.', type: 'action', action: 'repeatLastEdit' }, { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}}, { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}}, @@ -1136,7 +1136,6 @@ dom.importCssString(`.normal-mode .ace_cursor{ { name: 'global', shortName: 'g' } ]; - var Vim = function() { return vimApi; } //{ function enterVimMode(cm) { cm.setOption('disableInput', true); cm.setOption('showCursorWhenSelecting', false); @@ -1151,6 +1150,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ cm.off('cursorActivity', onCursorActivity); CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.state.vim = null; + if (highlightTimeout) clearTimeout(highlightTimeout); } function detachVimMap(cm, next) { @@ -1260,7 +1260,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ var lowerCaseAlphabet = makeKeyRange(97, 26); var numbers = makeKeyRange(48, 10); var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); - var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/']); + var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/', '+']); var upperCaseChars; try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); } catch (_) { upperCaseChars = /^[A-Z]$/; } @@ -1501,8 +1501,8 @@ dom.importCssString(`.normal-mode .ace_cursor{ register.clear(); this.latestRegister = registerName; if (cm.openDialog) { - this.onRecordingDone = cm.openDialog( - document.createTextNode('(recording)['+registerName+']'), null, {bottom:true}); + var template = dom('span', {class: 'cm-vim-message'}, 'recording @' + registerName); + this.onRecordingDone = cm.openDialog(template, null, {bottom:true}); } this.isRecording = true; } @@ -1575,7 +1575,9 @@ dom.importCssString(`.normal-mode .ace_cursor{ } var lastInsertModeKeyTimer; - var vimApi= { + var vimApi = { + enterVimMode: enterVimMode, + leaveVimMode: leaveVimMode, buildKeyMap: function() { // TODO: Convert keymap into dictionary format for fast lookup. }, @@ -1697,6 +1699,8 @@ dom.importCssString(`.normal-mode .ace_cursor{ return command(); } }, + multiSelectHandleKey: multiSelectHandleKey, + /** * This is the outermost function called by CodeMirror, after keys have * been mapped to their Vim equivalents. @@ -1724,13 +1728,17 @@ dom.importCssString(`.normal-mode .ace_cursor{ } function handleEsc() { if (key == '') { - // Clear input state and get back to normal mode. - clearInputState(cm); if (vim.visualMode) { + // Get back to normal mode. exitVisualMode(cm); } else if (vim.insertMode) { + // Get back to normal mode. exitInsertMode(cm); + } else { + // We're already in normal mode. Let '' be handled normally. + return; } + clearInputState(cm); return true; } } @@ -1798,10 +1806,10 @@ dom.importCssString(`.normal-mode .ace_cursor{ var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context); if (match.type == 'none') { clearInputState(cm); return false; } else if (match.type == 'partial') { return true; } - else if (match.type == 'clear') { clearInputState(cm); return true; } // ace_patch + else if (match.type == 'clear') { clearInputState(cm); return true; } vim.inputState.keyBuffer = ''; - var keysMatcher = /^(\d*)(.*)$/.exec(keys); + keysMatcher = /^(\d*)(.*)$/.exec(keys); if (keysMatcher[1] && keysMatcher[1] != '0') { vim.inputState.pushRepeatDigit(keysMatcher[1]); } @@ -1976,6 +1984,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ registers['.'] = new Register(); registers[':'] = new Register(); registers['/'] = new Register(); + registers['+'] = new Register(); } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise, blockwise) { @@ -2021,6 +2030,9 @@ dom.importCssString(`.normal-mode .ace_cursor{ } else { register.setText(text, linewise, blockwise); } + if (registerName === '+') { + navigator.clipboard.writeText(text); + } // The unnamed register always has the same value as the last used // register. this.unnamedRegister.setText(register.toString(), linewise); @@ -2103,7 +2115,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ } if (bestMatch.keys.slice(-11) == '') { var character = lastChar(keys); - if (!character || character.length > 1) return {type: 'clear'}; //ace_patch + if (!character || character.length > 1) return {type: 'clear'}; inputState.selectedCharacter = character; } return {type: 'full', command: bestMatch}; @@ -2347,6 +2359,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.reset(); exCommandDispatcher.processCommand(cm, input); + if (cm.state.vim) clearInputState(cm); } function onPromptKeyDown(e, input, close) { var keyName = CodeMirror.keyName(e), up, offset; @@ -2460,7 +2473,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ } if (vim.visualMode) { if (!(vim.visualBlock && newHead.ch === Infinity)) { - newHead = clipCursorToContent(cm, newHead); + newHead = clipCursorToContent(cm, newHead, oldHead); } if (newAnchor) { newAnchor = clipCursorToContent(cm, newAnchor); @@ -2476,7 +2489,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ cursorIsBefore(newAnchor, newHead) ? newHead : newAnchor); } else if (!operator) { - newHead = clipCursorToContent(cm, newHead); + newHead = clipCursorToContent(cm, newHead, oldHead); cm.setCursor(newHead.line, newHead.ch); } } @@ -2874,7 +2887,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ } var orig = cm.charCoords(head, 'local'); motionArgs.repeat = repeat; - var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); + curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); if (!curEnd) { return null; } @@ -3017,6 +3030,20 @@ dom.importCssString(`.normal-mode .ace_cursor{ } } else if (character === 't') { tmp = expandTagUnderCursor(cm, head, inclusive); + } else if (character === 's') { + // account for cursor on end of sentence symbol + var content = cm.getLine(head.line); + if (head.ch > 0 && isEndOfSentenceSymbol(content[head.ch])) { + head.ch -= 1; + } + var end = getSentence(cm, head, motionArgs.repeat, 1, inclusive) + var start = getSentence(cm, head, motionArgs.repeat, -1, inclusive) + // closer vim behaviour, 'a' only takes the space after the sentence if there is one before and after + if (isWhiteSpaceString(cm.getLine(start.line)[start.ch]) + && isWhiteSpaceString(cm.getLine(end.line)[end.ch -1])) { + start = {line: start.line, ch: start.ch + 1} + } + tmp = {start: start, end: end}; } else { // No text object defined for this, don't move. return null; @@ -3151,22 +3178,30 @@ dom.importCssString(`.normal-mode .ace_cursor{ }, indent: function(cm, args, ranges) { var vim = cm.state.vim; - var startLine = ranges[0].anchor.line; - var endLine = vim.visualBlock ? - ranges[ranges.length - 1].anchor.line : - ranges[0].head.line; - // In visual mode, n> shifts the selection right n times, instead of - // shifting n lines right once. - var repeat = (vim.visualMode) ? args.repeat : 1; - if (args.linewise) { - // The only way to delete a newline is to delete until the start of - // the next line, so in linewise mode evalInput will include the next - // line. We don't want this in indent, so we go back a line. - endLine--; - } - for (var i = startLine; i <= endLine; i++) { + if (cm.indentMore) { + var repeat = (vim.visualMode) ? args.repeat : 1; for (var j = 0; j < repeat; j++) { - cm.indentLine(i, args.indentRight); + if (args.indentRight) cm.indentMore(); + else cm.indentLess(); + } + } else { + var startLine = ranges[0].anchor.line; + var endLine = vim.visualBlock ? + ranges[ranges.length - 1].anchor.line : + ranges[0].head.line; + // In visual mode, n> shifts the selection right n times, instead of + // shifting n lines right once. + var repeat = (vim.visualMode) ? args.repeat : 1; + if (args.linewise) { + // The only way to delete a newline is to delete until the start of + // the next line, so in linewise mode evalInput will include the next + // line. We don't want this in indent, so we go back a line. + endLine--; + } + for (var i = startLine; i <= endLine; i++) { + for (var j = 0; j < repeat; j++) { + cm.indentLine(i, args.indentRight); + } } } return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); @@ -3283,11 +3318,14 @@ dom.importCssString(`.normal-mode .ace_cursor{ var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local'); var height = cm.getScrollInfo().clientHeight; var y = charCoords.top; - var lineHeight = charCoords.bottom - y; switch (actionArgs.position) { - case 'center': y = y - (height / 2) + lineHeight; + case 'center': y = charCoords.bottom - height / 2; break; - case 'bottom': y = y - height + lineHeight; + case 'bottom': + var lineLastCharPos = new Pos(lineNum, cm.getLine(lineNum).length - 1); + var lineLastCharCoords = cm.charCoords(lineLastCharPos, 'local'); + var lineHeight = lineLastCharCoords.bottom - y; + y = y - height + lineHeight break; } cm.scrollTo(null, y); @@ -3479,15 +3517,22 @@ dom.importCssString(`.normal-mode .ace_cursor{ var finalCh = 0; for (var i = curStart.line; i < curEnd.line; i++) { finalCh = lineLength(cm, curStart.line); - var tmp = new Pos(curStart.line + 1, - lineLength(cm, curStart.line + 1)); - var text = cm.getRange(curStart, tmp); - text = actionArgs.keepSpaces - ? text.replace(/\n\r?/g, '') - : text.replace(/\n\s*/g, ' '); - cm.replaceRange(text, curStart, tmp); - } - var curFinalPos = new Pos(curStart.line, finalCh); + var text = ''; + var nextStartCh = 0; + if (!actionArgs.keepSpaces) { + var nextLine = cm.getLine(curStart.line + 1); + nextStartCh = nextLine.search(/\S/); + if (nextStartCh == -1) { + nextStartCh = nextLine.length; + } else { + text = " "; + } + } + cm.replaceRange(text, + new Pos(curStart.line, finalCh), + new Pos(curStart.line + 1, nextStartCh)); + } + var curFinalPos = clipCursorToContent(cm, new Pos(curStart.line, finalCh)); if (vim.visualMode) { exitVisualMode(cm, false); } @@ -3512,10 +3557,19 @@ dom.importCssString(`.normal-mode .ace_cursor{ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); }, paste: function(cm, actionArgs, vim) { - var cur = copyCursor(cm.getCursor()); var register = vimGlobalState.registerController.getRegister( actionArgs.registerName); - var text = register.toString(); + if (actionArgs.registerName === '+') { + navigator.clipboard.readText().then((value) => { + this.continuePaste(cm, actionArgs, vim, value, register); + }) + } else { + var text = register.toString(); + this.continuePaste(cm, actionArgs, vim, text, register); + } + }, + continuePaste: function(cm, actionArgs, vim, text, register) { + var cur = copyCursor(cm.getCursor()); if (!text) { return; } @@ -3556,7 +3610,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ if (blockwise) { text = text.split('\n'); if (linewise) { - text.pop(); + text.pop(); } for (var i = 0; i < text.length; i++) { text[i] = (text[i] == '') ? ' ' : text[i]; @@ -3642,8 +3696,8 @@ dom.importCssString(`.normal-mode .ace_cursor{ // Now fine tune the cursor to where we want it. if (linewise && actionArgs.after) { curPosFinal = new Pos( - cur.line + 1, - findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); + cur.line + 1, + findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); } else if (linewise && !actionArgs.after) { curPosFinal = new Pos( cur.line, @@ -3665,7 +3719,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ undo: function(cm, actionArgs) { cm.operation(function() { repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); - cm.setCursor(cm.getCursor('anchor')); + cm.setCursor(clipCursorToContent(cm, cm.getCursor('start'))); }); }, redo: function(cm, actionArgs) { @@ -3784,14 +3838,28 @@ dom.importCssString(`.normal-mode .ace_cursor{ /** * Clips cursor to ensure that line is within the buffer's range + * and is not inside surrogate pair * If includeLineBreak is true, then allow cur.ch == lineLength. */ - function clipCursorToContent(cm, cur) { + function clipCursorToContent(cm, cur, oldCur) { var vim = cm.state.vim; var includeLineBreak = vim.insertMode || vim.visualMode; var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); - var maxCh = lineLength(cm, line) - 1 + !!includeLineBreak; + var text = cm.getLine(line); + var maxCh = text.length - 1 + !!includeLineBreak; var ch = Math.min(Math.max(0, cur.ch), maxCh); + // prevent cursor from entering surrogate pair + var charCode = text.charCodeAt(ch); + if (0xDC00 < charCode && charCode <0xDFFF) { + var direction = 1; + if (oldCur && oldCur.line == line) { + if (oldCur.ch > ch) { + direction = -1; + } + } + ch +=direction; + if (ch > maxCh) ch -=2; + } return new Pos(line, ch); } function copyArgs(args) { @@ -4503,7 +4571,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ /** * @param {CodeMirror} cm CodeMirror object. * @param {Pos} cur The position to start from. - * @param {Number} repeat Number of words to move past. + * @param {int} repeat Number of words to move past. * @param {boolean} forward True to search forward. False to search * backward. * @param {boolean} wordEnd True to move to end of word. False to move to @@ -4679,7 +4747,14 @@ dom.importCssString(`.normal-mode .ace_cursor{ return { start: start, end: end }; } - function findSentence(cm, cur, repeat, dir) { + /** + * Based on {@link findSentence}. The internal functions have the same names, + * but their behaviour is different. findSentence() crosses line breaks and + * is used for jumping to sentence beginnings before or after the current cursor position, + * whereas getSentence() is for getting the beginning or end of the sentence + * at the current cursor position, either including (a) or excluding (i) whitespace. + */ + function getSentence(cm, cur, repeat, dir, inclusive /*includes whitespace*/) { /* Takes an index object @@ -4689,10 +4764,152 @@ dom.importCssString(`.normal-mode .ace_cursor{ pos: index in line, dir: direction of traversal (-1 or 1) } - and modifies the line, ln, and pos members to represent the - next valid position or sets them to null if there are + and modifies the pos member to represent the + next valid position or sets the line to null if there are no more valid positions. */ + function nextChar(curr) { + if (curr.pos + curr.dir < 0 || curr.pos + curr.dir >= curr.line.length) { + curr.line = null; + } + else { + curr.pos += curr.dir; + } + } + /* + Performs one iteration of traversal in forward direction + Returns an index object of the sentence end + */ + function forward(cm, ln, pos, dir) { + var line = cm.getLine(ln); + + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + }; + + if (curr.line === "") { + return { ln: curr.ln, pos: curr.pos }; + } + + var lastSentencePos = curr.pos; + + // Move one step to skip character we start on + nextChar(curr); + + while (curr.line !== null) { + lastSentencePos = curr.pos; + if (isEndOfSentenceSymbol(curr.line[curr.pos])) { + if (!inclusive) { + return { ln: curr.ln, pos: curr.pos + 1 }; + } + else { + nextChar(curr); + while (curr.line !== null ) { + if (isWhiteSpaceString(curr.line[curr.pos])) { + lastSentencePos = curr.pos; + nextChar(curr) + } + else { + break; + } + } + return { ln: curr.ln, pos: lastSentencePos + 1 }; + } + } + nextChar(curr); + } + return { ln: curr.ln, pos: lastSentencePos + 1 }; + } + + /* + Performs one iteration of traversal in reverse direction + Returns an index object of the sentence start + */ + function reverse(cm, ln, pos, dir) { + var line = cm.getLine(ln); + + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + } + + if (curr.line === "") { + return { ln: curr.ln, pos: curr.pos }; + } + + var lastSentencePos = curr.pos; + + // Move one step to skip character we start on + nextChar(curr); + + while (curr.line !== null) { + if (!isWhiteSpaceString(curr.line[curr.pos]) && !isEndOfSentenceSymbol(curr.line[curr.pos])) { + lastSentencePos = curr.pos; + } + + else if (isEndOfSentenceSymbol(curr.line[curr.pos]) ) { + if (!inclusive) { + return { ln: curr.ln, pos: lastSentencePos }; + } + else { + if (isWhiteSpaceString(curr.line[curr.pos + 1])) { + return { ln: curr.ln, pos: curr.pos + 1 }; + } + else { + return { ln: curr.ln, pos: lastSentencePos }; + } + } + } + + nextChar(curr); + } + curr.line = line + if (inclusive && isWhiteSpaceString(curr.line[curr.pos])) { + return { ln: curr.ln, pos: curr.pos }; + } + else { + return { ln: curr.ln, pos: lastSentencePos }; + } + + } + + var curr_index = { + ln: cur.line, + pos: cur.ch, + }; + + while (repeat > 0) { + if (dir < 0) { + curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); + } + else { + curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); + } + repeat--; + } + + return new Pos(curr_index.ln, curr_index.pos); + } + + function findSentence(cm, cur, repeat, dir) { + + /* + Takes an index object + { + line: the line string, + ln: line number, + pos: index in line, + dir: direction of traversal (-1 or 1) + } + and modifies the line, ln, and pos members to represent the + next valid position or sets them to null if there are + no more valid positions. + */ function nextChar(cm, idx) { if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) { idx.ln += idx.dir; @@ -5180,16 +5397,16 @@ dom.importCssString(`.normal-mode .ace_cursor{ } /** - * hdom - Document Object Manipulator + * dom - Document Object Manipulator * Usage: - * hdom(''|[, ...{|<$styles>}||'']) + * dom(''|[, ...{|<$styles>}||'']) * Examples: - * hdom('div', {id:'xyz'}, hdom('p', 'CM rocks!', {$color:'red'})) - * hdom(document.head, hdom('script', 'alert("hello!")')) + * dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'})) + * dom(document.head, dom('script', 'alert("hello!")')) * Not supported: - * hdom('p', ['arrays are objects'], Error('objects specify attributes')) + * dom('p', ['arrays are objects'], Error('objects specify attributes')) */ - function hdom(n) { + function dom(n) { if (typeof n === 'string') n = document.createElement(n); for (var a, i = 1; i < arguments.length; i++) { if (!(a = arguments[i])) continue; @@ -5205,7 +5422,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ } function showConfirm(cm, template) { - var pre = hdom('span', {$color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message'}, template); //ace_patch span instead of pre + var pre = dom('div', {$color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message'}, template); if (cm.openNotification) { cm.openNotification(pre, {bottom: true, duration: 5000}); } else { @@ -5214,12 +5431,12 @@ dom.importCssString(`.normal-mode .ace_cursor{ } function makePrompt(prefix, desc) { - return hdom(document.createDocumentFragment(), - hdom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre'}, + return dom(document.createDocumentFragment(), + dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre'}, prefix, - hdom('input', {type: 'text', autocorrect: 'off', + dom('input', {type: 'text', autocorrect: 'off', autocapitalize: 'off', spellcheck: 'false'})), - desc && hdom('span', {$color: '#888'}, desc)); + desc && dom('span', {$color: '#888'}, desc)); } function showPrompt(cm, options) { @@ -5304,23 +5521,28 @@ dom.importCssString(`.normal-mode .ace_cursor{ query: query }; } + var highlightTimeout = 0; function highlightSearchMatches(cm, query) { - var searchState = getSearchState(cm); - var overlay = searchState.getOverlay(); - if (!overlay || query != overlay.query) { - if (overlay) { - cm.removeOverlay(overlay); - } - overlay = searchOverlay(query); - cm.addOverlay(overlay); - if (cm.showMatchesOnScrollbar) { - if (searchState.getScrollbarAnnotate()) { - searchState.getScrollbarAnnotate().clear(); + clearTimeout(highlightTimeout); + highlightTimeout = setTimeout(function() { + if (!cm.state.vim) return; + var searchState = getSearchState(cm); + var overlay = searchState.getOverlay(); + if (!overlay || query != overlay.query) { + if (overlay) { + cm.removeOverlay(overlay); + } + overlay = searchOverlay(query); + cm.addOverlay(overlay); + if (cm.showMatchesOnScrollbar) { + if (searchState.getScrollbarAnnotate()) { + searchState.getScrollbarAnnotate().clear(); + } + searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); } - searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); + searchState.setOverlay(overlay); } - searchState.setOverlay(overlay); - } + }, 50); } function findNext(cm, prev, query, repeat) { if (repeat === undefined) { repeat = 1; } @@ -6252,7 +6474,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ return; } showPrompt(cm, { - prefix: hdom('span', 'replace with ', hdom('strong', replaceWith), ' (y/n/a/q/l)'), + prefix: dom('span', 'replace with ', dom('strong', replaceWith), ' (y/n/a/q/l)'), onKeyDown: onPromptKeyDown }); } @@ -6601,11 +6823,9 @@ dom.importCssString(`.normal-mode .ace_cursor{ } resetVimGlobalState(); - //}; + // Initialize Vim and make it available as an API. - CodeMirror.Vim = Vim(); - - Vim = CodeMirror.Vim; + CodeMirror.Vim = vimApi; var specialKey = {'return':'CR',backspace:'BS','delete':'Del',esc:'Esc', left:'Left',right:'Right',up:'Up',down:'Down',space: 'Space',insert: 'Ins', @@ -6625,8 +6845,8 @@ dom.importCssString(`.normal-mode .ace_cursor{ if (name.length > 1) { name = '<' + name + '>'; } return name; } - var handleKey = Vim.handleKey.bind(Vim); - Vim.handleKey = function(cm, key, origin) { + var handleKey = vimApi.handleKey.bind(vimApi); + vimApi.handleKey = function(cm, key, origin) { return cm.operation(function() { return handleKey(cm, key, origin); }, true); @@ -6651,7 +6871,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ } function multiSelectHandleKey(cm, key, origin) { var isHandled = false; - var vim = Vim.maybeInitVimState_(cm); + var vim = vimApi.maybeInitVimState_(cm); var visualBlock = vim.visualBlock || vim.wasInVisualBlock; var wasMultiselect = cm.ace.inMultiSelectMode; @@ -6664,7 +6884,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ if (key == '' && !vim.insertMode && !vim.visualMode && wasMultiselect) { cm.ace.exitMultiSelectMode(); } else if (visualBlock || !wasMultiselect || cm.ace.inVirtualSelectionMode) { - isHandled = Vim.handleKey(cm, key, origin); + isHandled = vimApi.handleKey(cm, key, origin); } else { var old = cloneVimState(vim); cm.operation(function() { @@ -6697,7 +6917,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ return isHandled; } exports.CodeMirror = CodeMirror; - var getVim = Vim.maybeInitVimState_; + var getVim = vimApi.maybeInitVimState_; exports.handler = { $id: "ace/keyboard/vim", drawCursor: function(element, pixelPos, config, sel, session) { @@ -6717,9 +6937,9 @@ dom.importCssString(`.normal-mode .ace_cursor{ h = h / 2; top += h; } - dom.translate(element, left, top); - dom.setStyle(element.style, "width", w + "px"); - dom.setStyle(element.style, "height", h + "px"); + domLib.translate(element, left, top); + domLib.setStyle(element.style, "width", w + "px"); + domLib.setStyle(element.style, "height", h + "px"); }, handleKeyboard: function(data, hashId, key, keyCode, e) { var editor = data.editor; @@ -6858,14 +7078,14 @@ dom.importCssString(`.normal-mode .ace_cursor{ return status; } }; - Vim.defineOption({ + vimApi.defineOption({ name: "wrap", set: function(value, cm) { if (cm) {cm.ace.setOption("wrap", value)} }, type: "boolean" }, false); - Vim.defineEx('write', 'w', function() { + vimApi.defineEx('write', 'w', function() { console.log(':write is not implemented') }); defaultKeymap.push( @@ -6893,7 +7113,7 @@ dom.importCssString(`.normal-mode .ace_cursor{ type: 'operator', operator: 'hardWrap' }); - Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) { + vimApi.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) { var anchor = ranges[0].anchor.line; var head = ranges[0].head.line; if (operatorArgs.linewise) head--; @@ -6939,4 +7159,4 @@ dom.importCssString(`.normal-mode .ace_cursor{ exports.handler.defaultKeymap = defaultKeymap; exports.handler.actions = actions; - exports.Vim = Vim; + exports.Vim = vimApi; diff --git a/src/keyboard/vim_test.js b/src/keyboard/vim_test.js index c32cccf97a2..f22b50e4934 100644 --- a/src/keyboard/vim_test.js +++ b/src/keyboard/vim_test.js @@ -94,14 +94,38 @@ vim.CodeMirror.Vim.defineEx('write', 'w', function(cm) { // cm.replaceRange("x-", {ch: 4, line: 0}, {ch: 5, line: 0}); [editor.$vimModeHandler.cm.marks[0].find(),editor.$vimModeHandler.cm.marks[1].find()] var lineText, verbose = true; -var eqCursorPos = assert.deepEqual; -var eq = assert.equal; -var is = assert.ok; // ace_patch} + +function eqCursorPos(a, b) { + if (a.line != b.line || a.ch != b.ch) + throw failure( + "Expected cursor position " + + JSON.stringify([a.line, a.ch]) + + " to be equal to " + + JSON.stringify([b.line, b.ch]), + eqCursorPos + ); +} +function eq(a, b) { + if(a != b) + throw failure("Expected " + a + " to be equal to " + b, eq); +} +function is(a) { + if (!a) throw failure("Expected " + a + " to be truthy", is); +} +function failure(message, root) { + var error = new Error(message); + if (Error.captureStackTrace) + Error.captureStackTrace(error, root); + return error; +} + var Pos = CodeMirror.Pos; CodeMirror.Vim.suppressErrorLogging = true; +var isOldCodeMirror = /^5\./.test(CodeMirror.version); + var code = '' + ' wOrd1 (#%\n' + ' word3] \n' + @@ -295,7 +319,8 @@ function testVim(name, run, opts, expectedFail) { return CodeMirror.Vim.getRegisterController(); }, getNotificationText: function() { - return cm.getWrapperElement().querySelector(".cm-vim-message").textContent; + var container = cm.getWrapperElement().querySelector(".cm-vim-message"); + return container && container.textContent; } } CodeMirror.Vim.resetVimGlobalState_(); @@ -415,6 +440,7 @@ testMotion('|_repeat', ['3', '|'], makeCursor(0, 2), makeCursor(0,4)); testMotion('h', 'h', makeCursor(0, 0), word1.start); testMotion('h_repeat', ['3', 'h'], offsetCursor(word1.end, 0, -3), word1.end); testMotion('l', 'l', makeCursor(0, 1)); +testMotion('Space', 'Space', makeCursor(0, 1)); testMotion('l_repeat', ['2', 'l'], makeCursor(0, 2)); testMotion('j', 'j', offsetCursor(word1.end, 1, 0), word1.end); testMotion('j_repeat', ['2', 'j'], offsetCursor(word1.end, 2, 0), word1.end); @@ -589,15 +615,15 @@ testVim('gj_gk', function(cm, vim, helpers) { // Test bottom of document edge case. helpers.doKeys('100', 'g', 'j'); var endingPos = cm.getCursor(); - is(endingPos != 0, 'gj should not be on wrapped line 0'); + is(endingPos.ch != 0, 'gj should not be on wrapped line 0'); var topLeftCharCoords = cm.charCoords(makeCursor(0, 0)); var endingCharCoords = cm.charCoords(endingPos); is(topLeftCharCoords.left == endingCharCoords.left, 'gj should end up on column 0'); },{ lineNumbers: false, lineWrapping:true, value: 'Thislineisintentionallylongtotestmovementofgjandgkoverwrappedlines.' }); testVim('g0_g$', function(cm, vim, helpers) { + cm.setSize(120); var topLeftCharCoords = cm.charCoords(makeCursor(0, 0)); cm.setCursor(0, 4); - cm.setSize(120); helpers.doKeys('g', 'Down'); var secondLineCoords = cm.charCoords(cm.getCursor()); is(secondLineCoords.top > topLeftCharCoords.top); @@ -617,7 +643,7 @@ testVim('g0_g$', function(cm, vim, helpers) { is(startCoords.left < endCoords.left); is(startCoords.top == endCoords.top); is(start.ch < end.ch && end.ch < cm.getValue().length / 2); -},{ lineNumbers: false, lineWrapping:true, value: 'This line is intentionally long to test movement of g$ and g0 over wrapped lines.' }); +},{ lineNumbers: false, lineWrapping:true, value: 'This line is long to test movement of g$ and g0 over wrapped lines.' }); testVim('}', function(cm, vim, helpers) { cm.setCursor(0, 0); helpers.doKeys('}'); @@ -749,6 +775,86 @@ testVim('paragraph_motions', function(cm, vim, helpers) { eq('b\na\na\nc\n', register.toString()); }, { value: 'a\na\n\n\n\nb\nc\n\n\n\n\n\n\nd\n\ne\nf' }); +testVim('sentence_selections', function(cm, vim, helpers) { + // vis at beginning of line + cm.setCursor(0, 0); + helpers.doKeys('v', 'i', 's'); + eqCursorPos(new Pos(0, 0), cm.getCursor('anchor')); + eqCursorPos(new Pos(0, 14), cm.getCursor('head')); + + // vas at beginning of line + cm.setCursor(0, 0); + helpers.doKeys('v', 'a', 's'); + eqCursorPos(new Pos(0, 0), cm.getCursor('anchor')); + eqCursorPos(new Pos(0, 15), cm.getCursor('head')); + + // vis on sentence end + cm.setCursor(0, 13); + helpers.doKeys('v', 'i', 's'); + eqCursorPos(new Pos(0, 0), cm.getCursor('anchor')); + eqCursorPos(new Pos(0, 14), cm.getCursor('head')); + + // vas on sentence end + cm.setCursor(0, 13); + helpers.doKeys('v', 'a', 's'); + eqCursorPos(new Pos(0, 0), cm.getCursor('anchor')); + eqCursorPos(new Pos(0, 15), cm.getCursor('head')); + + // vis at sentence end, no whitespace after it + cm.setCursor(1, 18); + helpers.doKeys('v', 'i', 's'); + eqCursorPos(new Pos(1, 13), cm.getCursor('anchor')); + eqCursorPos(new Pos(1, 19), cm.getCursor('head')); + + // vas at sentence end, no whitespace after it + cm.setCursor(1, 18); + helpers.doKeys('v', 'a', 's'); + eqCursorPos(new Pos(1, 12), cm.getCursor('anchor')); + eqCursorPos(new Pos(1, 19), cm.getCursor('head')); + + // vis at sentence beginning, on whitespace + cm.setCursor(0, 14); + helpers.doKeys('v', 'i', 's'); + eqCursorPos(new Pos(0, 14), cm.getCursor('anchor')); + eqCursorPos(new Pos(0, 29), cm.getCursor('head')); + + cm.setCursor(0, 0); + helpers.doKeys('d', 'i', 's'); + var register = helpers.getRegisterController().getRegister(); + eq('Test sentence.', register.toString()); + + // return to original value + helpers.doKeys('u') + + cm.setCursor(0, 0); + helpers.doKeys('d', 'a', 's'); + register = helpers.getRegisterController().getRegister(); + eq('Test sentence. ', register.toString()); + + // return to original value + helpers.doKeys('u') + + cm.setCursor(1, 20); + helpers.doKeys('c', 'a', 's', ''); + register = helpers.getRegisterController().getRegister(); + eq('Test.', register.toString()); + + // return to original value + helpers.doKeys('u') + + cm.setCursor(3, 11); + helpers.doKeys('y', 'a', 's'); + register = helpers.getRegisterController().getRegister(); + eq('This is more text. ', register.toString()); + + cm.setCursor(3, 31); + helpers.doKeys('y', 'a', 's'); + register = helpers.getRegisterController().getRegister(); + eq(' No end of sentence symbol', register.toString()); + +}, { value: 'Test sentence. Test question?\nAgain.Never. Again.Test.\n\nHello. This is more text. No end of sentence symbol\n' }); + + // Operator tests testVim('dl', function(cm, vim, helpers) { var curStart = makeCursor(0, 0); @@ -1387,7 +1493,14 @@ testVim('._delete_visualBlock', function(cm, vim, helpers) { eq('ve\n\nsome\nsugar', cm.getValue()); helpers.doKeys('j', 'j', '.'); eq('ve\n\nome\nugar', cm.getValue()); - helpers.doKeys('u', '', '.'); + helpers.doKeys('u'); + if (!isOldCodeMirror) helpers.assertCursorAt(2, 0); + eq('ve\n\nsome\nsugar', cm.getValue()); + helpers.doKeys(''); + helpers.assertCursorAt(2, 0); + eq('ve\n\nome\nugar', cm.getValue()); + helpers.doKeys('.'); + helpers.assertCursorAt(2, 0); eq('ve\n\nme\ngar', cm.getValue()); },{value: 'give\nme\nsome\nsugar' }); testVim('>{motion}', function(cm, vim, helpers) { @@ -1442,13 +1555,12 @@ testVim('=', function(cm, vim, helpers) { eq(expectedValue, cm.getValue()); }, { value: ' word1\n word2\n word3', indentUnit: 2 }); -// Edit tests - configureCm is an optional argument that gives caller -// access to the cm object. -function testEdit(name, before, pos, edit, after, configureCm) { + +// Edit tests +function testEdit(name, before, pos, edit, after, opts) { + if (!opts) opts = {}; + opts.value = before; return testVim(name, function(cm, vim, helpers) { - if (configureCm) { - configureCm(cm); - } var ch = before.search(pos) var line = before.substring(0, ch).split('\n').length - 1; if (line) { @@ -1457,7 +1569,7 @@ function testEdit(name, before, pos, edit, after, configureCm) { cm.setCursor(line, ch); helpers.doKeys.apply(this, edit.split('')); eq(after, cm.getValue()); - }, {value: before}); + }, opts); } // These Delete tests effectively cover word-wise Change, Visual & Yank. @@ -1555,24 +1667,24 @@ testEdit('da>_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'da>', 'a\tb'); // deleting tag objects isAce || testEdit('dat_noop', 'hello', /n/, 'dat', 'hello'); -isAce || testEdit('dat_open_tag', 'hello', /n/, 'dat', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dat_open_tag', 'hello', /n/, 'dat', '', { + mode: 'xml' }); -isAce || testEdit('dat_inside_tag', 'hello', /l/, 'dat', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dat_inside_tag', 'hello', /l/, 'dat', '', { + mode: 'xml' }); -isAce || testEdit('dat_close_tag', 'hello', /\//, 'dat', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dat_close_tag', 'hello', /\//, 'dat', '', { + mode: 'xml' }); -isAce || testEdit('dit_open_tag', 'hello', /n/, 'dit', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dit_open_tag', 'hello', /n/, 'dit', '', { + mode: 'xml' }); -isAce || testEdit('dit_inside_tag', 'hello', /l/, 'dit', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dit_inside_tag', 'hello', /l/, 'dit', '', { + mode: 'xml' }); -isAce || testEdit('dit_close_tag', 'hello', /\//, 'dit', '', function(cm) { - cm.setOption('mode', 'xml'); +isAce || testEdit('dit_close_tag', 'hello', /\//, 'dit', '', { + mode: 'xml' }); function testSelection(name, before, pos, keys, sel) { @@ -1690,7 +1802,7 @@ testVim('/ search forward', function(cm, vim, helpers) { helpers.assertCursorAt(0, 11); }); }, {value: '__jmp1 jmp2 jmp'}); -isAce || testVim('insert_ctrl_u', function(cm, vim, helpers) { +testVim('insert_ctrl_u', function(cm, vim, helpers) { var curStart = makeCursor(0, 10); cm.setCursor(curStart); helpers.doKeys('a'); @@ -2008,11 +2120,11 @@ testVim('r_visual_block', function(cm, vim, helpers) { eq('1 l\n5 l\nalllefg', cm.getValue()); cm.setCursor(2, 0); helpers.doKeys('o'); - helpers.doKeys('\t\t') + helpers.doKeys('\t\t'); helpers.doKeys(''); helpers.doKeys('', 'h', 'h', 'r', 'r'); eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue()); -}, {value: '1234\n5678\nabcdefg'}); +}, {value: '1234\n5678\nabcdefg', indentWithTabs: true}); testVim('R', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('R'); @@ -2065,10 +2177,12 @@ testVim('mark\'', function(cm, vim, helpers) { helpers.assertCursorAt(1, 1); // edits helpers.doKeys('g', 'I', '\n', '', 'l'); + // the column may be different depending on editor behavior in insert mode + var ch = cm.getCursor().ch; helpers.doKeys('`', '`'); helpers.assertCursorAt(7, 2); helpers.doKeys('`', '`'); - helpers.assertCursorAt(2, 1); + helpers.assertCursorAt(2, ch); }); testVim('mark.', function(cm, vim, helpers) { cm.setCursor(0, 0); @@ -2476,9 +2590,29 @@ testVim('visual_join', function(cm, vim, helpers) { }, { 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()); + eq('1 2 3 4 5 6', cm.getValue()); is(!vim.visualMode); }, { value: '1\n2\n3\n4\n5\n6\n'}); +testVim('visual_join_blank', function(cm, vim, helpers) { + var initialValue = cm.getValue(); + helpers.doKeys('G', 'V', 'g', 'g', 'J'); + eq('1 2 5 6', cm.getValue()); + is(!vim.visualMode); + helpers.doKeys('u'); + eq(initialValue, cm.getValue()); + helpers.doKeys('G', 'V', 'g', 'g', 'g', 'J'); + eq('1 \t2\t 5 6', cm.getValue()); + helpers.doKeys('u'); + eq(cm.getCursor().line, 0); + eq(initialValue, cm.getValue()); + helpers.doKeys('J', 'J', 'J'); + helpers.assertCursorAt(0, 3); + helpers.doKeys('J'); + helpers.assertCursorAt(0, 4); + eq('1 2 5\n 6\n', cm.getValue()); + helpers.doKeys('u'); + eq('1 2\n5\n 6\n', cm.getValue()); +}, { value: '1 \n\t2\n\t \n\n5\n 6\n'}); testVim('visual_blank', function(cm, vim, helpers) { helpers.doKeys('v', 'k'); eq(vim.visualMode, true); @@ -2825,7 +2959,7 @@ testVim('?_nongreedy', function(cm, vim, helpers) { helpers.doKeys('n'); helpers.assertCursorAt(0, 4); helpers.doKeys('n'); - helpers.assertCursorAt(0, 0); // ace_patch TODO this appears to be wrong in codemirror + helpers.assertCursorAt(0, isOldCodeMirror ? 1 : 0); }, { value: 'aaa aa \n a aa'}); testVim('/_greedy', function(cm, vim, helpers) { helpers.doKeys('/', 'a+', '\n'); @@ -3028,7 +3162,9 @@ testVim('macro_insert', function(cm, vim, helpers) { helpers.doKeys('q', 'a', '0', 'i'); helpers.doKeys('foo') helpers.doKeys(''); + eq(helpers.getNotificationText(), 'recording @a'); helpers.doKeys('q', '@', 'a'); + eq(helpers.getNotificationText(), null); eq('foofoo', cm.getValue()); }, { value: ''}); testVim('macro_insert_repeat', function(cm, vim, helpers) { @@ -3649,6 +3785,32 @@ testVim('Ty,;', function(cm, vim, helpers) { helpers.doKeys('y', ',', 'P'); eq('01230123456789', cm.getValue()); }, { value: '0123456789'}); +testVim('page_motions', function(cm, vim, helpers) { + var value = "x".repeat(200).split("").map((_, i)=>i).join("\n"); + cm.setValue(value); + cm.refresh(); + var lines = 10; + var textHeight = cm.defaultTextHeight(); + cm.setSize(600, lines*textHeight); + cm.setCursor(100, 0); + cm.refresh(); + helpers.doKeys(''); + helpers.assertCursorAt(95, 0); + helpers.doKeys(''); + helpers.assertCursorAt(90, 0); + helpers.doKeys(''); + helpers.doKeys(''); + helpers.assertCursorAt(100, 0); + cm.refresh(); + helpers.doKeys(''); + cm.refresh(); + helpers.assertCursorAt(110, 0); + + helpers.doKeys(''); + cm.refresh(); + helpers.assertCursorAt(100, 0); + eq(value, cm.getValue()); +}); testVim('HML', function(cm, vim, helpers) { cm.refresh(); var lines = 35; @@ -3672,6 +3834,7 @@ testVim('HML', function(cm, vim, helpers) { })()}); var zVals = []; +var cursorIndexVals = []; forEach(['zb','zz','zt','z-','z.','z'], function(e, idx){ var lineNum = 250; var lines = 35; @@ -3681,11 +3844,16 @@ forEach(['zb','zz','zt','z-','z.','z'], function(e, idx){ var k2 = e.substring(1); var textHeight = cm.defaultTextHeight(); cm.setSize(600, lines*textHeight); - cm.setCursor(lineNum, 0); + cm.setCursor(lineNum, 1); + var originalCursorIndex = cm.indexFromPos(cm.getCursor()); helpers.doKeys(k1, k2); zVals[idx] = cm.getScrollInfo().top; + cursorIndexVals[idx] = { + before: originalCursorIndex, + after: cm.indexFromPos(cm.getCursor()) + }; }, { value: (function(){ - return new Array(500).join('\n'); + return new Array(500).join('12\n'); })()}); }); testVim('zb_to_bottom', function(cm, vim, helpers){ @@ -3724,6 +3892,33 @@ testVim('zz==z.', function(cm, vim, helpers){ testVim('zt==z', function(cm, vim, helpers){ eq(zVals[2], zVals[5]); }); +testVim('zt_no_cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[2]; + eq(cursorIndexes.before, cursorIndexes.after); +}); +testVim('z_cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[5]; + eq(cursorIndexes.before, 751); + eq(cursorIndexes.after, 750); +}); +testVim('zz_no_cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[1]; + eq(cursorIndexes.before, cursorIndexes.after); +}); +testVim('z._cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[4]; + eq(cursorIndexes.before, 751); + eq(cursorIndexes.after, 750); +}); +testVim('zb_no_cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[0]; + eq(cursorIndexes.before, cursorIndexes.after); +}); +testVim('z-_cursor_change', function(cm, vim, helpers){ + var cursorIndexes = cursorIndexVals[3]; + eq(cursorIndexes.before, 751); + eq(cursorIndexes.after, 750); +}); var moveTillCharacterSandbox = 'The quick brown fox \n'; @@ -3774,17 +3969,21 @@ var scrollMotionSandbox = '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'; testVim('scrollMotion', function(cm, vim, helpers){ var prevCursor, prevScrollInfo; + cm.setSize(320, 200); cm.setCursor(0, 0); + cm.refresh(); // ctrl-y at the top of the file should have no effect. helpers.doKeys(''); eq(0, cm.getCursor().line); + cm.refresh(); prevScrollInfo = cm.getScrollInfo(); helpers.doKeys(''); eq(1, cm.getCursor().line); + cm.refresh(); is(prevScrollInfo.top < cm.getScrollInfo().top); // Jump to the end of the sandbox. cm.setCursor(1000, 0); - cm.refresh(); //ace_patch + cm.refresh(); prevCursor = cm.getCursor(); // ctrl-e at the bottom of the file should have no effect. helpers.doKeys(''); @@ -4797,6 +4996,15 @@ testVim('ex_map_key2key_from_colon', function(cm, vim, helpers) { CodeMirror.Vim.mapclear(); }, { value: 'abc' }); +testVim('map in normal mode', function(cm, vim, helpers) { + CodeMirror.Vim.noremap('', 'i', 'normal'); + helpers.doKeys(''); + is(vim.insertMode, "Didn't switch to insert mode."); + helpers.doKeys(''); + is(!vim.insertMode, "Didn't switch to normal mode."); + CodeMirror.Vim.mapclear(); +}); + testVim('noremap', function(cm, vim, helpers) { CodeMirror.Vim.noremap(';', 'l'); cm.setCursor(0, 0); @@ -5131,22 +5339,29 @@ var typeKey = function() { Backquote: [192, "`", "~"], Minus: [189, "-", "_"], Equal: [187, "=", "+"], BracketLeft: [219, "[", "{"], Backslash: [220, "\\", "|"], BracketRight: [221, "]", "}"], Semicolon: [186, ";", ":"], Quote: [222, "'", '"'], Comma: [188, ",", "<"], - Period: [190, ".", ">"], Slash: [191, "/", "?"], Space: [32, " "], NumpadAdd: [107, "+"], + Period: [190, ".", ">"], Slash: [191, "/", "?"], Space: [32, " ", " "], NumpadAdd: [107, "+"], NumpadDecimal: [110, "."], NumpadSubtract: [109, "-"], NumpadDivide: [111, "/"], NumpadMultiply: [106, "*"] }; for (var i in specialKeys) { var key = specialKeys[i]; printableKeys[i] = printableKeys[key[1]] = shiftedKeys[key[2]] = key[0]; keyCodeToCode[key[0]] = i; + keyCodeToKey[key[0]] = key[1]; + keyCodeToKey["s-" + key[0]] = key[2]; } for (var i = 0; i < 10; i++) { - printableKeys[i] = shiftedKeys["!@#$%^&*()"[i]] = 48 + i; + var shifted = "!@#$%^&*()"[i]; + printableKeys[i] = shiftedKeys[shifted] = 48 + i; keyCodeToCode[48 + i] = "Digit" + i; + keyCodeToKey[48 + i] = i.toString(); + keyCodeToKey["s-" + (48 + i)] = shifted; } for (var i = 65; i < 91; i++) { var chr = String.fromCharCode(i + 32); printableKeys[chr] = shiftedKeys[chr.toUpperCase()] = i; keyCodeToCode[i] = "Key" + chr.toUpperCase(); + keyCodeToKey[i] = chr; + keyCodeToKey["s-" + i] = chr.toUpperCase(); } for (var i = 1; i < 13; i++) { controlKeys["F" + i] = 111 + i; @@ -5156,6 +5371,7 @@ var typeKey = function() { keyCodeToKey[controlKeys[i]] = i; keyCodeToCode[controlKeys[i]] = i; } + controlKeys["\t"] = controlKeys.Tab; controlKeys["\n"] = controlKeys.Return; controlKeys.Del = controlKeys.Delete; controlKeys.Esc = controlKeys.Escape; @@ -5198,6 +5414,10 @@ var typeKey = function() { text = text.toUpperCase(); } + if (keyCodeToKey[keyCode] != text && keyCodeToKey["s-" + keyCode] == text) { + shift = true; + } + var target = document.activeElement; var prevented = emit("keydown", true); if (isModifier) return; @@ -5222,11 +5442,11 @@ var typeKey = function() { data.charCode = text.charCodeAt(0); data.keyCode = type == "keypress" ? data.charCode : keyCode; data.which = data.keyCode; - data.shiftKey = shift || shiftedKeys[text]; + data.shiftKey = shift || (shiftedKeys[text] && !printableKeys[text]); data.ctrlKey = ctrl; data.altKey = alt; data.metaKey = meta; - data.key = text || keyCodeToKey[keyCode]; + data.key = keyCodeToKey[(shift ? "s-" : "") + keyCode] || console.error(text); data.code = keyCodeToCode[keyCode]; var event = new KeyboardEvent(type, data); @@ -5236,6 +5456,7 @@ var typeKey = function() { } function updateTextInput() { if (target._handleInputEventForTest) { + if (!isTextInput) return; return target._handleInputEventForTest(text); } var isTextarea = "selectionStart" in target && typeof target.value == "string";