diff --git a/kitchen-sink.html b/kitchen-sink.html index b2ab3577..65fe2a2a 100644 --- a/kitchen-sink.html +++ b/kitchen-sink.html @@ -12,8 +12,8 @@ --> - + + @@ -96,7 +96,6 @@ diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 13aa2f60..59b136b1 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -2476,7 +2476,6 @@ var Editor = function(renderer, session) { **/ this.undo = function() { this.$blockScrolling++; - this.session.$syncInformUndoManager(); this.session.getUndoManager().undo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); @@ -2488,7 +2487,6 @@ var Editor = function(renderer, session) { **/ this.redo = function() { this.$blockScrolling++; - this.session.$syncInformUndoManager(); this.session.getUndoManager().redo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); diff --git a/lib/ace/keyboard/vim.js b/lib/ace/keyboard/vim.js index 0e860db1..cbf3dda3 100644 --- a/lib/ace/keyboard/vim.js +++ b/lib/ace/keyboard/vim.js @@ -62,7 +62,7 @@ define(function(require, exports, module) { 'use strict'; - /* function log() { + function log() { var d = ""; function format(p) { if (typeof p != "object") @@ -83,7 +83,7 @@ define(function(require, exports, module) { d+= f+" " } console.log(d) - } */ + } var Range = require("../range").Range; var EventEmitter = require("../lib/event_emitter").EventEmitter; var dom = require("../lib/dom"); @@ -297,8 +297,10 @@ define(function(require, exports, module) { ranges.push(ranges.splice(primIndex, 1)[0]); } sel.toSingleRange(ranges[0].clone()); + var session = this.ace.session; for (var i = 0; i < ranges.length; i++) { - sel.addRange(ranges[i]); + var range = session.$clipRangeToDocument(ranges[i]); // todo why ace doesn't do this? + sel.addRange(range); } }; this.setSelection = function(a, h, options) { @@ -951,10 +953,14 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Operator-Motion dual commands { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }}, { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, - { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, - { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, - { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }}, - { keys: '~', type: 'operatorMotion', operator: 'changeCase', operatorArgs: { shouldMoveCursor: true }, motion: 'moveByCharacters', motionArgs: { forward: true }}, + { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, + { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, + { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { 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: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' }, // Actions { keys: '', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }}, @@ -965,7 +971,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' }, { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' }, { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' }, - { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank' }}, + { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' }, + { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' }, { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' }, { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' }, { keys: 'v', type: 'action', action: 'toggleVisualMode' }, @@ -1069,7 +1076,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.on('keypress', handleKeyPress); cm.on('keydown', handleKeyDown); - CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor"); } function leaveVimMode(cm) { @@ -1350,7 +1356,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ visualLine: false, visualBlock: false, lastSelection: null, - lastPastedText: null + lastPastedText: null, + sel: { + } }; } return cm.state.vim; @@ -1406,6 +1414,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs, ctx); }, + unmap: function(lhs, ctx) { + // remove user defined key bindings. + exCommandDispatcher.unmap(lhs, ctx); + }, setOption: setOption, getOption: getOption, defineOption: defineOption, @@ -1529,6 +1541,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ } catch (e) { // clear VIM state in case it's in a bad state. cm.state.vim = undefined; + maybeInitVimState(cm); throw e; } }); @@ -2032,13 +2045,13 @@ dom.importCssString(".normal-mode .ace_cursor{\ var operator = inputState.operator; var operatorArgs = inputState.operatorArgs || {}; var registerName = inputState.registerName; - var selectionEnd = copyCursor(cm.getCursor('head')); - var selectionStart = copyCursor(cm.getCursor('anchor')); - // The difference between cur and selection cursors are that cur is - // being operated on and ignores that there is a selection. - var curStart = copyCursor(selectionEnd); - var curOriginal = copyCursor(curStart); - var curEnd; + var sel = vim.sel; + // TODO: Make sure cm and vim selections are identical outside visual mode. + var origHead = copyCursor(vim.visualMode ? sel.head: cm.getCursor('head')); + var origAnchor = copyCursor(vim.visualMode ? sel.anchor : cm.getCursor('anchor')); + var oldHead = copyCursor(origHead); + var oldAnchor = copyCursor(origAnchor); + var newHead, newAnchor; var repeat; if (operator) { this.recordLastEdit(vim, inputState); @@ -2065,7 +2078,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ motionArgs.repeat = repeat; clearInputState(cm); if (motion) { - var motionResult = motions[motion](cm, motionArgs, vim); + var motionResult = motions[motion](cm, origHead, motionArgs, vim); vim.lastMotion = motions[motion]; if (!motionResult) { return; @@ -2078,159 +2091,139 @@ dom.importCssString(".normal-mode .ace_cursor{\ recordJumpPosition(cm, cachedCursor, motionResult); delete jumpList.cachedCursor; } else { - recordJumpPosition(cm, curOriginal, motionResult); + recordJumpPosition(cm, origHead, motionResult); } } if (motionResult instanceof Array) { - curStart = motionResult[0]; - curEnd = motionResult[1]; + newAnchor = motionResult[0]; + newHead = motionResult[1]; } else { - curEnd = motionResult; + newHead = motionResult; } // TODO: Handle null returns from motion commands better. - if (!curEnd) { - curEnd = Pos(curStart.line, curStart.ch); + if (!newHead) { + newHead = copyCursor(origHead); } if (vim.visualMode) { - // Check if the selection crossed over itself. Will need to shift - // the start point if that happened. - // offset is set to -1 or 1 to shift the curEnd - // left or right - var offset = 0; - if (cursorIsBefore(selectionStart, selectionEnd) && - (cursorEqual(selectionStart, curEnd) || - cursorIsBefore(curEnd, selectionStart))) { - // The end of the selection has moved from after the start to - // before the start. We will shift the start right by 1. - selectionStart.ch += 1; - offset = -1; - } else if (cursorIsBefore(selectionEnd, selectionStart) && - (cursorEqual(selectionStart, curEnd) || - cursorIsBefore(selectionStart, curEnd))) { - // The opposite happened. We will shift the start left by 1. - selectionStart.ch -= 1; - offset = 1; - } - // in case of visual Block selectionStart and curEnd - // may not be on the same line, - // Also, In case of v_o this should not happen. - if (!vim.visualBlock && !(motionResult instanceof Array)) { - curEnd.ch += offset; - } - if (vim.lastHPos != Infinity) { - vim.lastHPos = curEnd.ch; - } - selectionEnd = curEnd; - selectionStart = (motionResult instanceof Array) ? curStart : selectionStart; - if (vim.visualLine) { - if (cursorIsBefore(selectionStart, selectionEnd)) { - selectionStart.ch = 0; - - var lastLine = cm.lastLine(); - if (selectionEnd.line > lastLine) { - selectionEnd.line = lastLine; - } - selectionEnd.ch = lineLength(cm, selectionEnd.line); - } else { - selectionEnd.ch = 0; - selectionStart.ch = lineLength(cm, selectionStart.line); - } - } else if (vim.visualBlock) { - // Select a block and - // return the diagonally opposite end. - selectionStart = selectBlock(cm, selectionEnd); - } - if (!vim.visualBlock) { - cm.setSelection(selectionStart, selectionEnd); + newHead = clipCursorToContent(cm, newHead, true); + if (newAnchor) { + newAnchor = clipCursorToContent(cm, newAnchor, true); } + newAnchor = newAnchor || oldAnchor; + sel.anchor = newAnchor; + sel.head = newHead; + updateCmSelection(cm); updateMark(cm, vim, '<', - cursorIsBefore(selectionStart, selectionEnd) ? selectionStart - : selectionEnd); + cursorIsBefore(newAnchor, newHead) ? newAnchor + : newHead); updateMark(cm, vim, '>', - cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd - : selectionStart); + cursorIsBefore(newAnchor, newHead) ? newHead + : newAnchor); } else if (!operator) { - curEnd = clipCursorToContent(cm, curEnd); - cm.setCursor(curEnd.line, curEnd.ch); + newHead = clipCursorToContent(cm, newHead); + cm.setCursor(newHead.line, newHead.ch); } } - if (operator) { - var inverted = false; - vim.lastMotion = null; - var lastSelection = vim.lastSelection; - operatorArgs.repeat = repeat; // Indent in visual mode needs this. - if (vim.visualMode) { - curStart = selectionStart; - curEnd = selectionEnd; - motionArgs.inclusive = true; - operatorArgs.shouldMoveCursor = false; - } - // Swap start and end if motion was backward. - if (curEnd && cursorIsBefore(curEnd, curStart)) { - var tmp = curStart; - curStart = curEnd; - curEnd = tmp; - inverted = true; - } else if (!curEnd) { - curEnd = copyCursor(curStart); - } - if (motionArgs.inclusive && !vim.visualMode) { - // Move the selection end one to the right to include the last - // character. - curEnd.ch++; - } - if (operatorArgs.selOffset) { + if (operatorArgs.lastSel) { // Replaying a visual mode operation - curEnd.line = curStart.line + operatorArgs.selOffset.line; - if (operatorArgs.selOffset.line) {curEnd.ch = operatorArgs.selOffset.ch; } - else { curEnd.ch = curStart.ch + operatorArgs.selOffset.ch; } - // In case of blockwise visual - if (lastSelection && lastSelection.visualBlock) { - var block = lastSelection.visualBlock; - var width = block.width; - var height = block.height; - curEnd = Pos(curStart.line + height, curStart.ch + width); - // selectBlock creates a 'proper' rectangular block. - // We do not want that in all cases, so we manually set selections. - var selections = []; - for (var i = curStart.line; i < curEnd.line; i++) { - var anchor = Pos(i, curStart.ch); - var head = Pos(i, curEnd.ch); - var range = {anchor: anchor, head: head}; - selections.push(range); - } - cm.setSelections(selections); - var blockSelected = true; + newAnchor = oldAnchor; + var lastSel = operatorArgs.lastSel; + var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); + var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); + if (lastSel.visualLine) { + // Linewise Visual mode: The same number of lines. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); + } else if (lastSel.visualBlock) { + // Blockwise Visual mode: The same number of lines and columns. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); + } else if (lastSel.head.line == lastSel.anchor.line) { + // Normal Visual mode within one line: The same number of characters. + newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset); + } else { + // Normal Visual mode with several lines: The same number of lines, in the + // last line the same number of characters as in the last line the last time. + newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); } + vim.visualMode = true; + vim.visualLine = lastSel.visualLine; + vim.visualBlock = lastSel.visualBlock; + sel = vim.sel = { + anchor: newAnchor, + head: newHead + }; + updateCmSelection(cm); } else if (vim.visualMode) { - var selOffset = Pos(); - selOffset.line = curEnd.line - curStart.line; - if (selOffset.line) { selOffset.ch = curEnd.ch; } - else { selOffset.ch = curEnd.ch - curStart.ch; } - operatorArgs.selOffset = selOffset; + operatorArgs.lastSel = { + anchor: copyCursor(sel.anchor), + head: copyCursor(sel.head), + visualBlock: vim.visualBlock, + visualLine: vim.visualLine + }; } - var linewise = motionArgs.linewise || - (vim.visualMode && vim.visualLine) || - operatorArgs.linewise; - if (linewise) { - // Expand selection to entire line. - expandSelectionToLine(cm, curStart, curEnd); - } else if (motionArgs.forward) { - // Clip to trailing newlines only if the motion goes forward. - clipToLine(cm, curStart, curEnd); + var curStart, curEnd, linewise, mode; + var cmSel; + if (vim.visualMode) { + // Init visual op + curStart = cursorMin(sel.head, sel.anchor); + curEnd = cursorMax(sel.head, sel.anchor); + linewise = vim.visualLine || operatorArgs.linewise; + mode = vim.visualBlock ? 'block' : + linewise ? 'line' : + 'char'; + cmSel = makeCmSelection(cm, { + anchor: curStart, + head: curEnd + }, mode); + if (linewise) { + var ranges = cmSel.ranges; + if (mode == 'block') { + // Linewise operators in visual block mode extend to end of line + for (var i = 0; i < ranges.length; i++) { + ranges[i].head.ch = lineLength(cm, ranges[i].head.line); + } + } else if (mode == 'line') { + ranges[0].head = Pos(ranges[0].head.line + 1, 0); + } + } + } else { + // Init motion op + curStart = copyCursor(newAnchor || oldAnchor); + curEnd = copyCursor(newHead || oldHead); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curStart; + curStart = curEnd; + curEnd = tmp; + } + linewise = motionArgs.linewise || operatorArgs.linewise; + if (linewise) { + // Expand selection to entire line. + expandSelectionToLine(cm, curStart, curEnd); + } else if (motionArgs.forward) { + // Clip to trailing newlines only if the motion goes forward. + clipToLine(cm, curStart, curEnd); + } + mode = 'char'; + var exclusive = !motionArgs.inclusive || linewise; + cmSel = makeCmSelection(cm, { + anchor: curStart, + head: curEnd + }, mode, exclusive); } + cm.setSelections(cmSel.ranges, cmSel.primary); + vim.lastMotion = null; + operatorArgs.repeat = repeat; // For indent in visual mode. operatorArgs.registerName = registerName; // Keep track of linewise as it affects how paste and change behave. operatorArgs.linewise = linewise; - if (!vim.visualBlock && !blockSelected) { - cm.setSelection(curStart, curEnd); - } - operators[operator](cm, operatorArgs, vim, curStart, - curEnd, curOriginal); + var operatorMoveTo = operators[operator]( + cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); if (vim.visualMode) { exitVisualMode(cm); } + if (operatorMoveTo) { + cm.setCursor(operatorMoveTo); + } } }, recordLastEdit: function(vim, inputState, actionCommand) { @@ -2249,7 +2242,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ */ // All of the functions below return Cursor objects. var motions = { - moveToTopLine: function(cm, motionArgs) { + moveToTopLine: function(cm, _head, motionArgs) { var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, @@ -2258,17 +2251,17 @@ dom.importCssString(".normal-mode .ace_cursor{\ var line = Math.floor((range.top + range.bottom) * 0.5); return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, - moveToBottomLine: function(cm, motionArgs) { + moveToBottomLine: function(cm, _head, motionArgs) { var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, - expandToLine: function(cm, motionArgs) { + expandToLine: function(_cm, head, motionArgs) { // Expands forward to end of line, and then to next line if repeat is // >1. Does not handle backward motion! - var cur = cm.getCursor(); + var cur = head; return Pos(cur.line + motionArgs.repeat - 1, Infinity); }, - findNext: function(cm, motionArgs) { + findNext: function(cm, _head, motionArgs) { var state = getSearchState(cm); var query = state.getQuery(); if (!query) { @@ -2280,7 +2273,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ highlightSearchMatches(cm, query); return findNext(cm, prev/** prev */, query, motionArgs.repeat); }, - goToMark: function(cm, motionArgs, vim) { + goToMark: function(cm, _head, motionArgs, vim) { var mark = vim.marks[motionArgs.selectedCharacter]; if (mark) { var pos = mark.find(); @@ -2288,22 +2281,19 @@ dom.importCssString(".normal-mode .ace_cursor{\ } return null; }, - moveToOtherHighlightedEnd: function(cm, motionArgs, vim) { - var ranges = cm.listSelections(); - var curEnd = cm.getCursor('head'); - var curStart = ranges[0].anchor; - var curIndex = cursorEqual(ranges[0].head, curEnd) ? ranges.length-1 : 0; - if (motionArgs.sameLine && vim.visualBlock) { - curStart = Pos(curEnd.line, ranges[curIndex].anchor.ch); - curEnd = Pos(ranges[curIndex].head.line, curEnd.ch); + moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { + if (vim.visualBlock && motionArgs.sameLine) { + var sel = vim.sel; + return [ + clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)), + clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch)) + ]; } else { - curStart = ranges[curIndex].anchor; + return ([vim.sel.head, vim.sel.anchor]); } - cm.setCursor(curEnd); - return ([curEnd, curStart]); }, - jumpToMark: function(cm, motionArgs, vim) { - var best = cm.getCursor(); + jumpToMark: function(cm, head, motionArgs, vim) { + var best = head; for (var i = 0; i < motionArgs.repeat; i++) { var cursor = best; for (var key in vim.marks) { @@ -2340,14 +2330,14 @@ dom.importCssString(".normal-mode .ace_cursor{\ } return best; }, - moveByCharacters: function(cm, motionArgs) { - var cur = cm.getCursor(); + moveByCharacters: function(_cm, head, motionArgs) { + var cur = head; var repeat = motionArgs.repeat; var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; return Pos(cur.line, ch); }, - moveByLines: function(cm, motionArgs, vim) { - var cur = cm.getCursor(); + moveByLines: function(cm, head, motionArgs, vim) { + var cur = head; var endCh = cur.ch; // Depending what our last motion was, we may want to do different // things. If our last motion was moving vertically, we want to @@ -2382,8 +2372,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; return Pos(line, endCh); }, - moveByDisplayLines: function(cm, motionArgs, vim) { - var cur = cm.getCursor(); + moveByDisplayLines: function(cm, head, motionArgs, vim) { + var cur = head; switch (vim.lastMotion) { case this.moveByDisplayLines: case this.moveByScroll: @@ -2410,16 +2400,16 @@ dom.importCssString(".normal-mode .ace_cursor{\ vim.lastHPos = res.ch; return res; }, - moveByPage: function(cm, motionArgs) { + moveByPage: function(cm, head, motionArgs) { // CodeMirror only exposes functions that move the cursor page down, so // doing this bad hack to move the cursor and move it back. evalInput // will move the cursor to where it should be in the end. - var curStart = cm.getCursor(); + var curStart = head; var repeat = motionArgs.repeat; return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); }, - moveByParagraph: function(cm, motionArgs) { - var line = cm.getCursor().line; + moveByParagraph: function(cm, head, motionArgs) { + var line = head.line; var repeat = motionArgs.repeat; var inc = motionArgs.forward ? 1 : -1; for (var i = 0; i < repeat; i++) { @@ -2434,16 +2424,16 @@ dom.importCssString(".normal-mode .ace_cursor{\ } return Pos(line, 0); }, - moveByScroll: function(cm, motionArgs, vim) { + moveByScroll: function(cm, head, motionArgs, vim) { var scrollbox = cm.getScrollInfo(); var curEnd = null; var repeat = motionArgs.repeat; if (!repeat) { repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); } - var orig = cm.charCoords(cm.getCursor(), 'local'); + var orig = cm.charCoords(head, 'local'); motionArgs.repeat = repeat; - var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); + var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); if (!curEnd) { return null; } @@ -2451,11 +2441,11 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.scrollTo(null, scrollbox.top + dest.top - orig.top); return curEnd; }, - moveByWords: function(cm, motionArgs) { - return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward, + moveByWords: function(cm, head, motionArgs) { + return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, !!motionArgs.wordEnd, !!motionArgs.bigWord); }, - moveTillCharacter: function(cm, motionArgs) { + moveTillCharacter: function(cm, _head, motionArgs) { var repeat = motionArgs.repeat; var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter); @@ -2465,26 +2455,26 @@ dom.importCssString(".normal-mode .ace_cursor{\ curEnd.ch += increment; return curEnd; }, - moveToCharacter: function(cm, motionArgs) { + moveToCharacter: function(cm, head, motionArgs) { var repeat = motionArgs.repeat; recordLastCharacterSearch(0, motionArgs); return moveToCharacter(cm, repeat, motionArgs.forward, - motionArgs.selectedCharacter) || cm.getCursor(); + motionArgs.selectedCharacter) || head; }, - moveToSymbol: function(cm, motionArgs) { + moveToSymbol: function(cm, head, motionArgs) { var repeat = motionArgs.repeat; return findSymbol(cm, repeat, motionArgs.forward, - motionArgs.selectedCharacter) || cm.getCursor(); + motionArgs.selectedCharacter) || head; }, - moveToColumn: function(cm, motionArgs, vim) { + moveToColumn: function(cm, head, motionArgs, vim) { var repeat = motionArgs.repeat; // repeat is equivalent to which column we want to move to! vim.lastHPos = repeat - 1; - vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left; + vim.lastHSPos = cm.charCoords(head,'div').left; return moveToColumn(cm, repeat); }, - moveToEol: function(cm, motionArgs, vim) { - var cur = cm.getCursor(); + moveToEol: function(cm, head, motionArgs, vim) { + var cur = head; vim.lastHPos = Infinity; var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); var end=cm.clipPos(retval); @@ -2492,15 +2482,15 @@ dom.importCssString(".normal-mode .ace_cursor{\ vim.lastHSPos = cm.charCoords(end,'div').left; return retval; }, - moveToFirstNonWhiteSpaceCharacter: function(cm) { + moveToFirstNonWhiteSpaceCharacter: function(cm, head) { // Go to the start of the line where the text begins, or the end for // whitespace-only lines - var cursor = cm.getCursor(); + var cursor = head; return Pos(cursor.line, findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); }, - moveToMatchedSymbol: function(cm) { - var cursor = cm.getCursor(); + moveToMatchedSymbol: function(cm, head) { + var cursor = head; var line = cursor.line; var ch = cursor.ch; var lineText = cm.getLine(line); @@ -2521,11 +2511,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ return cursor; } }, - moveToStartOfLine: function(cm) { - var cursor = cm.getCursor(); - return Pos(cursor.line, 0); + moveToStartOfLine: function(_cm, head) { + return Pos(head.line, 0); }, - moveToLineOrEdgeOfDocument: function(cm, motionArgs) { + moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); if (motionArgs.repeatIsExplicit) { lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); @@ -2533,7 +2522,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ return Pos(lineNum, findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); }, - textObjectManipulation: function(cm, motionArgs) { + textObjectManipulation: function(cm, head, motionArgs) { // TODO: lots of possible exceptions that can be thrown here. Try da( // outside of a () block. @@ -2562,9 +2551,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ var tmp; if (mirroredPairs[character]) { - tmp = selectCompanionObject(cm, character, inclusive); + tmp = selectCompanionObject(cm, head, character, inclusive); } else if (selfPaired[character]) { - tmp = findBeginningAndEnd(cm, character, inclusive); + tmp = findBeginningAndEnd(cm, head, character, inclusive); } else if (character === 'W') { tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, true /** bigWord */); @@ -2586,7 +2575,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ } }, - repeatLastCharacterSearch: function(cm, motionArgs) { + repeatLastCharacterSearch: function(cm, head, motionArgs) { var lastSearch = vimGlobalState.lastChararacterSearch; var repeat = motionArgs.repeat; var forward = motionArgs.forward === lastSearch.forward; @@ -2596,141 +2585,107 @@ dom.importCssString(".normal-mode .ace_cursor{\ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); if (!curEnd) { cm.moveH(increment, 'char'); - return cm.getCursor(); + return head; } curEnd.ch += increment; return curEnd; } }; + function fillArray(val, times) { + var arr = []; + for (var i = 0; i < times; i++) { + arr.push(val); + } + return arr; + } + /** + * An operator acts on a text selection. It receives the list of selections + * as input. The corresponding CodeMirror selection is guaranteed to + * match the input selection. + */ var operators = { - change: function(cm, operatorArgs, vim) { - var selections = cm.listSelections(); - var start = selections[0], end = selections[selections.length-1]; - var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; - var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; - var text = cm.getSelection(); - var visualBlock = vim.visualBlock; - if (vim.lastSelection && !vim.visualMode) { - visualBlock = vim.lastSelection.visualBlock ? true : visualBlock; - } - var lastInsertModeChanges = vimGlobalState.macroModeState.lastInsertModeChanges; - lastInsertModeChanges.inVisualBlock = visualBlock; - var replacement = new Array(selections.length).join('1').split('1'); - // save the selectionEnd mark - var selectionEnd = vim.marks[">"] && vim.marks[">"].find(); - if (!selectionEnd) { - selectionEnd = cm.getCursor("head"); - } - vimGlobalState.registerController.pushText( - operatorArgs.registerName, 'change', text, - operatorArgs.linewise); - if (operatorArgs.linewise) { - // 'C' in visual block extends the block till eol for all lines - if (visualBlock){ - var startLine = curStart.line; - while (startLine <= curEnd.line) { - var endCh = lineLength(cm, startLine); - var head = Pos(startLine, endCh); - var anchor = Pos(startLine, curStart.ch); - startLine++; - cm.replaceRange('', anchor, head); - } - } else { - // Push the next line back down, if there is a next line. - replacement = '\n'; - if (curEnd.line == curStart.line && curEnd.line == cm.lastLine()) { - replacement = ''; - } - cm.replaceRange(replacement, curStart, curEnd); - cm.indentLine(curStart.line, 'smart'); - // null ch so setCursor moves to end of line. - curStart.ch = null; - cm.setCursor(curStart); - } - } else { - // Exclude trailing whitespace if the range is not all whitespace. - var text = cm.getRange(curStart, curEnd); + change: function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock; + if (!vim.visualMode) { + var anchor = ranges[0].anchor, + head = ranges[0].head; + text = cm.getRange(anchor, head); if (!isWhiteSpaceString(text)) { + // Exclude trailing whitespace if the range is not all whitespace. var match = (/\s+$/).exec(text); if (match) { - curEnd = offsetCursor(curEnd, 0, - match[0].length); + head = offsetCursor(head, 0, - match[0].length); + text = text.slice(0, - match[0].length); } } - if (visualBlock) { - cm.replaceSelections(replacement); - } else { - cm.setCursor(curStart); - cm.replaceRange('', curStart, curEnd); + var wasLastLine = head.line - 1 == cm.lastLine(); + cm.replaceRange('', anchor, head); + if (args.linewise && !wasLastLine) { + // Push the next line back down, if there is a next line. + CodeMirror.commands.newlineAndIndent(cm); + // null ch so setCursor moves to end of line. + anchor.ch = null; } + finalHead = anchor; + } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); + cm.replaceSelections(replacement); + finalHead = cursorMin(ranges[0].head, ranges[0].anchor); } - vim.marks['>'] = cm.setBookmark(selectionEnd); - actions.enterInsertMode(cm, {}, cm.state.vim); + vimGlobalState.registerController.pushText( + args.registerName, 'change', text, + args.linewise, ranges.length > 1); + actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); }, // delete is a javascript keyword. - 'delete': function(cm, operatorArgs, vim) { - var selections = cm.listSelections(); - var start = selections[0], end = selections[selections.length-1]; - var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; - var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; - // Save the '>' mark before cm.replaceRange clears it. - var selectionEnd, selectionStart; - var blockwise = vim.visualBlock; - if (vim.visualMode) { - selectionEnd = vim.marks['>'].find(); - selectionStart = vim.marks['<'].find(); - } else if (vim.lastSelection) { - selectionEnd = vim.lastSelection.curStartMark.find(); - selectionStart = vim.lastSelection.curEndMark.find(); - blockwise = vim.lastSelection.visualBlock; - } - var text = cm.getSelection(); - vimGlobalState.registerController.pushText( - operatorArgs.registerName, 'delete', text, - operatorArgs.linewise, blockwise); - var replacement = new Array(selections.length).join('1').split('1'); - // If the ending line is past the last line, inclusive, instead of - // including the trailing \n, include the \n before the starting line - if (operatorArgs.linewise && - curEnd.line == cm.lastLine() && curStart.line == curEnd.line) { - if (curEnd.line == 0) { - curStart.ch = 0; + 'delete': function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + if (!vim.visualBlock) { + var anchor = ranges[0].anchor, + head = ranges[0].head; + if (args.linewise && + head.line != cm.firstLine() && + anchor.line == cm.lastLine() && + anchor.line == head.line - 1) { + // Special case for dd on last line (and first line). + if (anchor.line == cm.firstLine()) { + anchor.ch = 0; + } else { + anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); + } } - else { - var tmp = copyCursor(curEnd); - curStart.line--; - curStart.ch = lineLength(cm, curStart.line); - curEnd = tmp; + text = cm.getRange(anchor, head); + cm.replaceRange('', anchor, head); + finalHead = anchor; + if (args.linewise) { + finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); } - cm.replaceRange('', curStart, curEnd); } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); cm.replaceSelections(replacement); + finalHead = ranges[0].anchor; } - // restore the saved bookmark - if (selectionEnd) { - var curStartMark = cm.setBookmark(selectionStart); - var curEndMark = cm.setBookmark(selectionEnd); - if (vim.visualMode) { - vim.marks['<'] = curStartMark; - vim.marks['>'] = curEndMark; - } else { - vim.lastSelection.curStartMark = curStartMark; - vim.lastSelection.curEndMark = curEndMark; - } - } - if (operatorArgs.linewise) { - cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); - } else { - cm.setCursor(curStart); - } + vimGlobalState.registerController.pushText( + args.registerName, 'delete', text, + args.linewise, vim.visualBlock); + return finalHead; }, - indent: function(cm, operatorArgs, vim, curStart, curEnd) { - var startLine = curStart.line; - var endLine = curEnd.line; + 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) ? operatorArgs.repeat : 1; - if (operatorArgs.linewise) { + 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. @@ -2738,17 +2693,15 @@ dom.importCssString(".normal-mode .ace_cursor{\ } for (var i = startLine; i <= endLine; i++) { for (var j = 0; j < repeat; j++) { - cm.indentLine(i, operatorArgs.indentRight); + cm.indentLine(i, args.indentRight); } } - cm.setCursor(curStart); - cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); }, - changeCase: function(cm, operatorArgs, _vim, _curStart, _curEnd, _curOriginal) { + changeCase: function(cm, args, ranges, oldAnchor, newHead) { var selections = cm.getSelections(); - var ranges = cm.listSelections(); var swapped = []; - var toLower = operatorArgs.toLower; + var toLower = args.toLower; for (var j = 0; j < selections.length; j++) { var toSwap = selections[j]; var text = ''; @@ -2766,18 +2719,26 @@ dom.importCssString(".normal-mode .ace_cursor{\ swapped.push(text); } cm.replaceSelections(swapped); - var curStart = ranges[0].anchor; - var curEnd = ranges[0].head; - if (!operatorArgs.shouldMoveCursor) { - cm.setCursor(cursorIsBefore(curStart, curEnd) ? curStart : curEnd); + if (args.shouldMoveCursor){ + return newHead; + } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { + return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); + } else if (args.linewise){ + return oldAnchor; + } else { + return cursorMin(ranges[0].anchor, ranges[0].head); } }, - yank: function(cm, operatorArgs, vim, _curStart, _curEnd, curOriginal) { + yank: function(cm, args, ranges, oldAnchor) { + var vim = cm.state.vim; var text = cm.getSelection(); + var endPos = vim.visualMode + ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) + : oldAnchor; vimGlobalState.registerController.pushText( - operatorArgs.registerName, 'yank', - text, operatorArgs.linewise, vim.visualBlock); - cm.setCursor(curOriginal); + args.registerName, 'yank', + text, args.linewise, vim.visualBlock); + return endPos; } }; @@ -2869,41 +2830,40 @@ dom.importCssString(".normal-mode .ace_cursor{\ vim.insertMode = true; vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; var insertAt = (actionArgs) ? actionArgs.insertAt : null; - if (vim.visualMode) { - var selections = getSelectedAreaRange(cm, vim); - var selectionStart = selections[0]; - var selectionEnd = selections[1]; - } + var sel = vim.sel; + var head = actionArgs.head || cm.getCursor('head'); + var height = cm.listSelections().length; if (insertAt == 'eol') { - var cursor = cm.getCursor(); - cursor = Pos(cursor.line, lineLength(cm, cursor.line)); - cm.setCursor(cursor); + head = Pos(head.line, lineLength(cm, head.line)); } else if (insertAt == 'charAfter') { - cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); + head = offsetCursor(head, 0, 1); } else if (insertAt == 'firstNonBlank') { - if (vim.visualMode && !vim.visualBlock) { - if (selectionEnd.line < selectionStart.line) { - cm.setCursor(selectionEnd); + head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head); + } else if (insertAt == 'startOfSelectedArea') { + if (!vim.visualBlock) { + if (sel.head.line < sel.anchor.line) { + head = sel.head; } else { - selectionStart = Pos(selectionStart.line, 0); - cm.setCursor(selectionStart); + head = Pos(sel.anchor.line, 0); } - cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); - } else if (vim.visualBlock) { - selectionEnd = Pos(selectionEnd.line, selectionStart.ch); - cm.setCursor(selectionStart); - selectBlock(cm, selectionEnd); } else { - cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + head = Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.min(sel.head.ch, sel.anchor.ch)); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; } } else if (insertAt == 'endOfSelectedArea') { - if (vim.visualBlock) { - selectionStart = Pos(selectionStart.line, selectionEnd.ch); - cm.setCursor(selectionStart); - selectBlock(cm, selectionEnd); - } else if (selectionEnd && selectionEnd.line < selectionStart.line) { - selectionEnd = Pos(selectionStart.line, 0); - cm.setCursor(selectionEnd); + if (!vim.visualBlock) { + if (sel.head.line >= sel.anchor.line) { + head = offsetCursor(sel.head, 0, 1); + } else { + head = Pos(sel.anchor.line, 0); + } + } else { + head = Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.max(sel.head.ch + 1, sel.anchor.ch)); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; } } else if (insertAt == 'inplace') { if (vim.visualMode){ @@ -2929,128 +2889,68 @@ dom.importCssString(".normal-mode .ace_cursor{\ if (vim.visualMode) { exitVisualMode(cm); } + selectForInsert(cm, head, height); }, toggleVisualMode: function(cm, actionArgs, vim) { var repeat = actionArgs.repeat; - var curStart = cm.getCursor(); - var curEnd; - var selections = cm.listSelections(); + var anchor = cm.getCursor(); + var head; // TODO: The repeat should actually select number of characters/lines // equal to the repeat times the size of the previous visual // operation. if (!vim.visualMode) { + // Entering visual mode vim.visualMode = true; vim.visualLine = !!actionArgs.linewise; vim.visualBlock = !!actionArgs.blockwise; - if (vim.visualLine) { - curStart.ch = 0; - curEnd = clipCursorToContent( - cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)), + head = clipCursorToContent( + cm, Pos(anchor.line, anchor.ch + repeat - 1), true /** includeLineBreak */); - } else { - curEnd = clipCursorToContent( - cm, Pos(curStart.line, curStart.ch + repeat), - true /** includeLineBreak */); - } - cm.setSelection(curStart, curEnd); - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); + vim.sel = { + anchor: anchor, + head: head + }; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + } else if (vim.visualLine ^ actionArgs.linewise || + vim.visualBlock ^ actionArgs.blockwise) { + // Toggling between modes + vim.visualLine = !!actionArgs.linewise; + vim.visualBlock = !!actionArgs.blockwise; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); } else { - curStart = cm.getCursor('anchor'); - curEnd = cm.getCursor('head'); - if (vim.visualLine) { - if (actionArgs.blockwise) { - // This means Ctrl-V pressed in linewise visual - vim.visualBlock = true; - selectBlock(cm, curEnd); - CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'}); - } else if (!actionArgs.linewise) { - // v pressed in linewise, switch to characterwise visual mode - CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'}); - } else { - exitVisualMode(cm); - } - vim.visualLine = false; - } else if (vim.visualBlock) { - if (actionArgs.linewise) { - // Shift-V pressed in blockwise visual mode - vim.visualLine = true; - curStart = Pos(selections[0].anchor.line, 0); - curEnd = Pos(selections[selections.length-1].anchor.line, lineLength(cm, selections[selections.length-1].anchor.line)); - cm.setSelection(curStart, curEnd); - CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'linewise'}); - } else if (!actionArgs.blockwise) { - // v pressed in blockwise mode, Switch to characterwise - if (curEnd != selections[0].head) { - curStart = selections[0].anchor; - } else { - curStart = selections[selections.length-1].anchor; - } - cm.setSelection(curStart, curEnd); - CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'}); - } else { - exitVisualMode(cm); - } - vim.visualBlock = false; - } else if (actionArgs.linewise) { - // Shift-V pressed in characterwise visual mode. Switch to linewise - // visual mode instead of exiting visual mode. - vim.visualLine = true; - curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 : - lineLength(cm, curStart.line); - curEnd.ch = cursorIsBefore(curStart, curEnd) ? - lineLength(cm, curEnd.line) : 0; - cm.setSelection(curStart, curEnd); - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"}); - } else if (actionArgs.blockwise) { - vim.visualBlock = true; - selectBlock(cm, curEnd); - CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'}); - } else { - exitVisualMode(cm); - } + exitVisualMode(cm); } - updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart - : curEnd); - updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd - : curStart); }, reselectLastSelection: function(cm, _actionArgs, vim) { - var curStart = vim.marks['<'].find(); - var curEnd = vim.marks['>'].find(); var lastSelection = vim.lastSelection; + if (vim.visualMode) { + updateLastSelection(cm, vim); + } if (lastSelection) { - // Set the selections as per last selection - var selectionStart = lastSelection.curStartMark.find(); - var selectionEnd = lastSelection.curEndMark.find(); - var blockwise = lastSelection.visualBlock; - // update last selection - updateLastSelection(cm, vim, curStart, curEnd); - if (blockwise) { - cm.setCursor(selectionStart); - selectionStart = selectBlock(cm, selectionEnd); - } else { - cm.setSelection(selectionStart, selectionEnd); - selectionStart = cm.getCursor('anchor'); - selectionEnd = cm.getCursor('head'); + var anchor = lastSelection.anchorMark.find(); + var head = lastSelection.headMark.find(); + if (!anchor || !head) { + // If the marks have been destroyed due to edits, do nothing. + return; } - if (vim.visualMode) { - updateMark(cm, vim, '<', cursorIsBefore(selectionStart, selectionEnd) ? selectionStart - : selectionEnd); - updateMark(cm, vim, '>', cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd - : selectionStart); - } - // Last selection is updated now + vim.sel = { + anchor: anchor, + head: head + }; vim.visualMode = true; - if (lastSelection.visualLine) { - vim.visualLine = true; - vim.visualBlock = false; - } else if (lastSelection.visualBlock) { - vim.visualLine = false; - vim.visualBlock = true; - } else { - vim.visualBlock = vim.visualLine = false; - } - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); + vim.visualLine = lastSelection.visualLine; + vim.visualBlock = lastSelection.visualBlock; + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + CodeMirror.signal(cm, 'vim-mode-change', { + mode: 'visual', + subMode: vim.visualLine ? 'linewise' : + vim.visualBlock ? 'blockwise' : ''}); } }, joinLines: function(cm, actionArgs, vim) { @@ -3067,18 +2967,19 @@ dom.importCssString(".normal-mode .ace_cursor{\ Infinity)); } var finalCh = 0; - cm.operation(function() { - for (var i = curStart.line; i < curEnd.line; i++) { - finalCh = lineLength(cm, curStart.line); - var tmp = Pos(curStart.line + 1, - lineLength(cm, curStart.line + 1)); - var text = cm.getRange(curStart, tmp); - text = text.replace(/\n\s*/g, ' '); - cm.replaceRange(text, curStart, tmp); - } - var curFinalPos = Pos(curStart.line, finalCh); - cm.setCursor(curFinalPos); - }); + for (var i = curStart.line; i < curEnd.line; i++) { + finalCh = lineLength(cm, curStart.line); + var tmp = Pos(curStart.line + 1, + lineLength(cm, curStart.line + 1)); + var text = cm.getRange(curStart, tmp); + text = text.replace(/\n\s*/g, ' '); + cm.replaceRange(text, curStart, tmp); + } + var curFinalPos = Pos(curStart.line, finalCh); + cm.setCursor(curFinalPos); + if (vim.visualMode) { + exitVisualMode(cm); + } }, newLineAndEnterInsertMode: function(cm, actionArgs, vim) { vim.insertMode = true; @@ -3174,7 +3075,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ var emptyStrings = new Array(selections.length).join('1').split('1'); // save the curEnd marker before it get cleared due to cm.replaceRange. if (vim.lastSelection) { - lastSelectionCurEnd = vim.lastSelection.curEndMark.find(); + lastSelectionCurEnd = vim.lastSelection.headMark.find(); } // push the previously selected text to unnamed register vimGlobalState.registerController.unnamedRegister.setText(selectedText); @@ -3198,7 +3099,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ } // restore the the curEnd marker if(lastSelectionCurEnd) { - vim.lastSelection.curEndMark = cm.setBookmark(lastSelectionCurEnd); + vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); } if (linewise) { curPosFinal.ch=0; @@ -3240,10 +3141,10 @@ dom.importCssString(".normal-mode .ace_cursor{\ } } } - cm.setCursor(curPosFinal); if (vim.visualMode) { exitVisualMode(cm); } + cm.setCursor(curPosFinal); }, undo: function(cm, actionArgs) { cm.operation(function() { @@ -3372,8 +3273,18 @@ dom.importCssString(".normal-mode .ace_cursor{\ return ret; } function offsetCursor(cur, offsetLine, offsetCh) { + if (typeof offsetLine === 'object') { + offsetCh = offsetLine.ch; + offsetLine = offsetLine.line; + } return Pos(cur.line + offsetLine, cur.ch + offsetCh); } + function getOffset(anchor, head) { + return { + line: head.line - anchor.line, + ch: head.line - anchor.line + }; + } function commandMatches(keys, keyMap, context, inputState) { // Partial matches are not applied. They inform the key handler // that the current key sequence is a subsequence of a valid key @@ -3446,9 +3357,15 @@ dom.importCssString(".normal-mode .ace_cursor{\ return false; } function cursorMin(cur1, cur2) { + if (arguments.length > 2) { + cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); + } return cursorIsBefore(cur1, cur2) ? cur1 : cur2; } function cursorMax(cur1, cur2) { + if (arguments.length > 2) { + cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); + } return cursorIsBefore(cur1, cur2) ? cur2 : cur1; } function cursorIsBetween(cur1, cur2, cur3) { @@ -3516,11 +3433,19 @@ dom.importCssString(".normal-mode .ace_cursor{\ selections.push(range); } primIndex = head.line == lastLine ? selections.length - 1 : 0; - cm.setSelections(selections, primIndex); + cm.setSelections(selections); selectionEnd.ch = headCh; base.ch = baseCh; return base; } + function selectForInsert(cm, head, height) { + var sel = []; + for (var i = 0; i < height; i++) { + var lineHead = offsetCursor(head, i, 0); + sel.push({anchor: lineHead, head: lineHead}); + } + cm.setSelections(sel, 0); + } // getIndex returns the index of the cursor in the selections. function getIndex(ranges, cursor, end) { for (var i = 0; i < ranges.length; i++) { @@ -3561,8 +3486,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ } cm.setSelections(selections); } else { - var start = lastSelection.curStartMark.find(); - var end = lastSelection.curEndMark.find(); + var start = lastSelection.anchorMark.find(); + var end = lastSelection.headMark.find(); var line = end.line - start.line; var ch = end.ch - start.ch; selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; @@ -3581,36 +3506,28 @@ dom.importCssString(".normal-mode .ace_cursor{\ return getCurrentSelectedAreaRange(); } } - function updateLastSelection(cm, vim, selectionStart, selectionEnd) { - if (!selectionStart || !selectionEnd) { - selectionStart = vim.marks['<'].find() || cm.getCursor('anchor'); - selectionEnd = vim.marks['>'].find() || cm.getCursor('head'); - } + // Updates the previous selection with the current selection's values. This + // should only be called in visual mode. + function updateLastSelection(cm, vim) { + var anchor = vim.sel.anchor; + var head = vim.sel.head; // To accommodate the effect of lastPastedText in the last selection if (vim.lastPastedText) { - selectionEnd = cm.posFromIndex(cm.indexFromPos(selectionStart) + vim.lastPastedText.length); + head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); vim.lastPastedText = null; } - var ranges = cm.listSelections(); - // This check ensures to set the cursor - // position where we left off in previous selection - var swap = getIndex(ranges, selectionStart, 'head') > -1; - if (vim.visualBlock) { - var height = Math.abs(selectionStart.line - selectionEnd.line)+1; - var width = Math.abs(selectionStart.ch - selectionEnd.ch); - var block = {height: height, width: width}; - } - // can't use selection state here because yank has already reset its cursor - // Also, Bookmarks make the visual selections robust to edit operations - vim.lastSelection = {'curStartMark': cm.setBookmark(swap ? selectionEnd : selectionStart), - 'curEndMark': cm.setBookmark(swap ? selectionStart : selectionEnd), + vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), + 'headMark': cm.setBookmark(head), + 'anchor': copyCursor(anchor), + 'head': copyCursor(head), 'visualMode': vim.visualMode, 'visualLine': vim.visualLine, - 'visualBlock': block}; + 'visualBlock': vim.visualBlock}; } function expandSelection(cm, start, end) { - var head = cm.getCursor('head'); - var anchor = cm.getCursor('anchor'); + var sel = cm.state.vim.sel; + var head = sel.head; + var anchor = sel.anchor; var tmp; if (cursorIsBefore(end, start)) { tmp = end; @@ -3623,9 +3540,75 @@ dom.importCssString(".normal-mode .ace_cursor{\ } else { anchor = cursorMin(start, anchor); head = cursorMax(head, end); + head = offsetCursor(head, 0, -1); + if (head.ch == -1 && head.line != cm.firstLine()) { + head = Pos(head.line - 1, lineLength(cm, head.line - 1)); + } } return [anchor, head]; } + /** + * Updates the CodeMirror selection to match the provided vim selection. + * If no arguments are given, it uses the current vim selection state. + */ + function updateCmSelection(cm, sel, mode) { + var vim = cm.state.vim; + sel = sel || vim.sel; + var mode = mode || + vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; + var cmSel = makeCmSelection(cm, sel, mode); + cm.setSelections(cmSel.ranges, cmSel.primary); + updateFakeCursor(cm); + } + function makeCmSelection(cm, sel, mode, exclusive) { + var head = copyCursor(sel.head); + var anchor = copyCursor(sel.anchor); + if (mode == 'char') { + var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + head = offsetCursor(sel.head, 0, headOffset); + anchor = offsetCursor(sel.anchor, 0, anchorOffset); + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'line') { + if (!cursorIsBefore(sel.head, sel.anchor)) { + anchor.ch = 0; + + var lastLine = cm.lastLine(); + if (head.line > lastLine) { + head.line = lastLine; + } + head.ch = lineLength(cm, head.line); + } else { + head.ch = 0; + anchor.ch = lineLength(cm, anchor.line); + } + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'block') { + var top = Math.min(anchor.line, head.line), + left = Math.min(anchor.ch, head.ch), + bottom = Math.max(anchor.line, head.line), + right = Math.max(anchor.ch, head.ch) + 1; + var height = bottom - top + 1; + var primary = head.line == top ? 0 : height - 1; + var ranges = []; + for (var i = 0; i < height; i++) { + ranges.push({ + anchor: Pos(top + i, left), + head: Pos(top + i, right) + }); + } + return { + ranges: ranges, + primary: primary + }; + } + } function getHead(cm) { var cur = cm.getCursor('head'); if (cm.getSelection().length == 1) { @@ -3636,22 +3619,20 @@ dom.importCssString(".normal-mode .ace_cursor{\ return cur; } - function exitVisualMode(cm) { + /** + * If moveHead is set to false, the CodeMirror selection will not be + * touched. The caller assumes the responsibility of putting the cursor + * in the right place. + */ + function exitVisualMode(cm, moveHead) { var vim = cm.state.vim; - var selectionStart = cm.getCursor('anchor'); - var selectionEnd = cm.getCursor('head'); - // hack to place the cursor at the right place - // in case of visual block - if (vim.visualBlock && (cursorIsBefore(selectionStart, selectionEnd))) { - selectionEnd.ch--; + if (moveHead !== false) { + cm.setCursor(clipCursorToContent(cm, vim.sel.head)); } updateLastSelection(cm, vim); vim.visualMode = false; vim.visualLine = false; vim.visualBlock = false; - if (!cursorEqual(selectionStart, selectionEnd)) { - cm.setCursor(clipCursorToContent(cm, selectionEnd)); - } CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); if (vim.fakeCursor) { vim.fakeCursor.clear(); @@ -3981,6 +3962,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ /** * @param {CodeMirror} cm CodeMirror object. + * @param {Pos} cur The position to start from. * @param {int} repeat Number of words to move past. * @param {boolean} forward True to search forward. False to search * backward. @@ -3990,8 +3972,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ * False if only alphabet characters count as part of the word. * @return {Cursor} The position the cursor should move to. */ - function moveToWord(cm, repeat, forward, wordEnd, bigWord) { - var cur = cm.getCursor(); + function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) { var curStart = copyCursor(cur); var words = []; if (forward && !wordEnd || !forward && wordEnd) { @@ -4091,8 +4072,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ // TODO: perhaps this finagling of start and end positions belonds // in codmirror/replaceRange? - function selectCompanionObject(cm, symb, inclusive) { - var cur = getHead(cm), start, end; + function selectCompanionObject(cm, head, symb, inclusive) { + var cur = head, start, end; var bracketRegexp = ({ '(': /[()]/, ')': /[()]/, @@ -4136,8 +4117,8 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Takes in a symbol and a cursor and tries to simulate text objects that // have identical opening and closing symbols // TODO support across multiple lines - function findBeginningAndEnd(cm, symb, inclusive) { - var cur = copyCursor(getHead(cm)); + function findBeginningAndEnd(cm, head, symb, inclusive) { + var cur = copyCursor(head); var line = cm.getLine(cur.line); var chars = line.split(''); var start, end, i, len; @@ -4560,13 +4541,6 @@ dom.importCssString(".normal-mode .ace_cursor{\ top: renderer.getFirstFullyVisibleRow(), bottom: renderer.getLastFullyVisibleRow() } - var scrollInfo = cm.getScrollInfo(); - var occludeToleranceTop = 6; - var occludeToleranceBottom = 10; - var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); - var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; - var to = cm.coordsChar({left:0, top: bottomY}, 'local'); - return {top: from.line, bottom: to.line}; } // Ex command handling @@ -5483,45 +5457,49 @@ dom.importCssString(".normal-mode .ace_cursor{\ // Cursor moved outside the context of an edit. Reset the change. lastChange.changes = []; } - } else { + } else if (!cm.curOp.isVimOp) { handleExternalSelection(cm, vim); } if (vim.visualMode) { - var from, head; - from = head = cm.getCursor('head'); - var anchor = cm.getCursor('anchor'); - var to = Pos(head.line, from.ch + (cursorIsBefore(anchor, head) ? -1 : 1)); - if (cursorIsBefore(to, from)) { - var temp = from; - from = to; - to = temp; - } - if (vim.fakeCursor) { - vim.fakeCursor.clear(); - } - vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'}); + updateFakeCursor(cm); } } - + function updateFakeCursor(cm) { + var vim = cm.state.vim; + var from = copyCursor(vim.sel.head); + var to = offsetCursor(from, 0, 1); + if (vim.fakeCursor) { + vim.fakeCursor.clear(); + } + vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'}); + } function handleExternalSelection(cm, vim) { var anchor = cm.getCursor('anchor'); var head = cm.getCursor('head'); // Enter or exit visual mode to match mouse selection. if (vim.visualMode && cursorEqual(head, anchor) && lineLength(cm, head.line) > head.ch) { - exitVisualMode(cm); - } else if (!cm.curOp.isVimOp && !vim.visualMode && !vim.insertMode && cm.somethingSelected()) { + exitVisualMode(cm, false); + } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { vim.visualMode = true; vim.visualLine = false; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); } - if (!cm.curOp.isVimOp) { - if (vim.visualMode) { - updateMark(cm, vim, '<', cursorMin(head, anchor)); - updateMark(cm, vim, '>', cursorMax(head, anchor)); - } else if (!vim.insertMode) { - // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. - vim.lastHPos = cm.getCursor().ch; - } + if (vim.visualMode) { + // Bind CodeMirror selection model to vim selection model. + // Mouse selections are considered visual characterwise. + var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; + var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; + head = offsetCursor(head, 0, headOffset); + anchor = offsetCursor(anchor, 0, anchorOffset); + vim.sel = { + anchor: anchor, + head: head + }; + updateMark(cm, vim, '<', cursorMin(head, anchor)); + updateMark(cm, vim, '>', cursorMax(head, anchor)); + } else if (!vim.insertMode) { + // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. + vim.lastHPos = cm.getCursor().ch; } } @@ -5613,19 +5591,21 @@ dom.importCssString(".normal-mode .ace_cursor{\ } return true; } - var curStart = cm.getCursor(); + var head = cm.getCursor('head'); var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock; if (inVisualBlock) { // Set up block selection again for repeating the changes. var vim = cm.state.vim; - var block = vim.lastSelection.visualBlock; - var curEnd = Pos(curStart.line + block.height-1, curStart.ch); - cm.setCursor(curStart); - selectBlock(cm, curEnd); + var lastSel = vim.lastSelection; + var offset = getOffset(lastSel.anchor, lastSel.head); + selectForInsert(cm, head, offset.line + 1); repeat = cm.listSelections().length; - cm.setCursor(curStart); + cm.setCursor(head); } for (var i = 0; i < repeat; i++) { + if (inVisualBlock) { + cm.setCursor(offsetCursor(head, i, 0)); + } for (var j = 0; j < changes.length; j++) { var change = changes[j]; if (change instanceof InsertModeKey) { @@ -5635,10 +5615,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.replaceRange(change, cur, cur); } } - if (inVisualBlock) { - curStart.line++; - cm.setCursor(curStart); - } + } + if (inVisualBlock) { + cm.setCursor(offsetCursor(head, 0, 1)); } } @@ -5683,6 +5662,12 @@ dom.importCssString(".normal-mode .ace_cursor{\ o = cloneVimState(o); n[key] = o; }); + if (state.sel) { + n.sel = { + head: state.sel.head && copyCursor(state.sel.head), + anchor: state.sel.anchor && copyCursor(state.sel.anchor) + }; + } return n; } function multiSelectHandleKey(cm, key, origin) { @@ -5705,6 +5690,15 @@ dom.importCssString(".normal-mode .ace_cursor{\ cm.ace.forEachSelection(function() { var sel = cm.ace.selection; cm.state.vim.lastHPos = sel.$desiredColumn == null ? sel.lead.column : sel.$desiredColumn; + var head = cm.getCursor("head"); + var anchor = cm.getCursor("anchor"); + var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; + var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; + head = offsetCursor(head, 0, headOffset); + anchor = offsetCursor(anchor, 0, anchorOffset); + cm.state.vim.sel.head = head; + cm.state.vim.sel.anchor = anchor; + isHandled = handleKey(cm, key, origin); sel.$desiredColumn = cm.state.vim.lastHPos == -1 ? null : cm.state.vim.lastHPos; if (cm.virtualSelectionMode()) { @@ -5860,7 +5854,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ // on mac, with some keyboard layouts (e.g swedish) ^ starts composition, we don't need it in normal mode updateMacCompositionHandlers: function(editor, enable) { var onCompositionUpdateOverride = function(text) { - if (util.currentMode !== "insert") { + var cm = editor.state.cm; + var vim = getVim(cm); + if (!vim.insertMode) { var el = this.textInput.getElement(); el.blur(); el.focus(); @@ -5870,7 +5866,9 @@ dom.importCssString(".normal-mode .ace_cursor{\ } }; var onCompositionStartOverride = function(text) { - if (util.currentMode === "insert") { + var cm = editor.state.cm; + var vim = getVim(cm); + if (!vim.insertMode) { this.onCompositionStartOrig(text); } }; @@ -5947,7 +5945,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ ace.off("beforeEndOperation", delayedExecAceCommand); var cmd = ace.state.cm.vimCmd; if (cmd) { - ace.execCommand(cmd.name, cmd.args); + ace.execCommand(cmd.exec ? cmd : cmd.name, cmd.args); } ace.curOp = ace.prevOp; } @@ -5956,5 +5954,7 @@ dom.importCssString(".normal-mode .ace_cursor{\ ][(actionArgs.all ? 2 : 0) + (actionArgs.open ? 1 : 0)]); }, + exports.handler.defaultKeymap = defaultKeymap; + Vim.map("Y", "yy"); }); diff --git a/lib/ace/keyboard/vim_test.js b/lib/ace/keyboard/vim_test.js index 1fd8c320..8c9eeb65 100644 --- a/lib/ace/keyboard/vim_test.js +++ b/lib/ace/keyboard/vim_test.js @@ -72,6 +72,9 @@ function test(name, fn) { exports["test " + name] = fn; } +vim.CodeMirror.Vim.unmap("Y"); + + // cm.setBookmark({ch: 5, line: 0}) // cm.setBookmark({ch: 4, line: 0}) @@ -985,32 +988,36 @@ testVim('cc_multiply_repeat', function(cm, vim, helpers) { is(register.linewise); eq('vim-insert', cm.getOption('keyMap')); }); -testVim('cc_append', function(cm, vim, helpers) { +testVim('cc_should_not_append_to_document', function(cm, vim, helpers) { var expectedLineCount = cm.lineCount(); cm.setCursor(cm.lastLine(), 0); helpers.doKeys('c', 'c'); eq(expectedLineCount, cm.lineCount()); }); +function fillArray(val, times) { + var arr = []; + for (var i = 0; i < times; i++) { + arr.push(val); + } + return arr; +} testVim('c_visual_block', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('', '2', 'j', 'l', 'l', 'l', 'c'); - var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' '); - replacement.pop(); + var replacement = fillArray('hello', 3); cm.replaceSelections(replacement); eq('1hello\n5hello\nahellofg', cm.getValue()); helpers.doKeys(''); cm.setCursor(2, 3); helpers.doKeys('', '2', 'k', 'h', 'C'); - replacement = new Array(cm.listSelections().length+1).join('world ').split(' '); - replacement.pop(); + replacement = fillArray('world', 3); cm.replaceSelections(replacement); eq('1hworld\n5hworld\nahworld', cm.getValue()); }, {value: '1234\n5678\nabcdefg'}); testVim('c_visual_block_replay', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('', '2', 'j', 'l', 'c'); - var replacement = new Array(cm.listSelections().length+1).join('fo ').split(' '); - replacement.pop(); + var replacement = fillArray('fo', 3); cm.replaceSelections(replacement); eq('1fo4\n5fo8\nafodefg', cm.getValue()); helpers.doKeys(''); @@ -1019,6 +1026,17 @@ testVim('c_visual_block_replay', function(cm, vim, helpers) { eq('foo4\nfoo8\nfoodefg', cm.getValue()); }, {value: '1234\n5678\nabcdefg'}); +testVim('d_visual_block', function(cm, vim, helpers) { + cm.setCursor(0, 1); + helpers.doKeys('', '2', 'j', 'l', 'l', 'l', 'd'); + eq('1\n5\nafg', cm.getValue()); +}, {value: '1234\n5678\nabcdefg'}); +testVim('D_visual_block', function(cm, vim, helpers) { + cm.setCursor(0, 1); + helpers.doKeys('', '2', 'j', 'l', 'D'); + eq('1\n5\na', cm.getValue()); +}, {value: '1234\n5678\nabcdefg'}); + // Swapcase commands edit in place and do not modify registers. testVim('g~w_repeat', function(cm, vim, helpers) { // Assert that dw does delete newline if it should go to the next line, and @@ -1042,7 +1060,7 @@ testVim('g~g~', function(cm, vim, helpers) { var register = helpers.getRegisterController().getRegister(); eq('', register.toString()); is(!register.linewise); - eqPos({line: curStart.line, ch:0}, cm.getCursor()); + eqPos(curStart, cm.getCursor()); }, { value: ' word1\nword2\nword3\nword4\nword5\nword6' }); testVim('gu_and_gU', function(cm, vim, helpers) { var curStart = makeCursor(0, 7); @@ -1350,7 +1368,7 @@ testVim('a_eol', function(cm, vim, helpers) { helpers.assertCursorAt(0, lines[0].length); eq('vim-insert', cm.getOption('keyMap')); }); -testVim('a_endOfSelectedArea', function(cm, vim, helpers) { +testVim('A_endOfSelectedArea', function(cm, vim, helpers) { cm.setCursor(0, 0); helpers.doKeys('v', 'j', 'l'); helpers.doKeys('A'); @@ -1813,6 +1831,49 @@ testVim('visual', function(cm, vim, helpers) { helpers.doKeys('d'); eq('15', cm.getValue()); }, { value: '12345' }); +testVim('visual_yank', function(cm, vim, helpers) { + helpers.doKeys('v', '3', 'l', 'y'); + helpers.assertCursorAt(0, 0); + helpers.doKeys('p'); + eq('aa te test for yank', cm.getValue()); +}, { value: 'a test for yank' }) +testVim('visual_w', function(cm, vim, helpers) { + helpers.doKeys('v', 'w'); + eq(cm.getSelection(), 'motion t'); +}, { value: 'motion test'}); +testVim('visual_initial_selection', function(cm, vim, helpers) { + cm.setCursor(0, 1); + helpers.doKeys('v'); + cm.getSelection('n'); +}, { value: 'init'}); +testVim('visual_crossover_left', function(cm, vim, helpers) { + cm.setCursor(0, 2); + helpers.doKeys('v', 'l', 'h', 'h'); + cm.getSelection('ro'); +}, { value: 'cross'}); +testVim('visual_crossover_left', function(cm, vim, helpers) { + cm.setCursor(0, 2); + helpers.doKeys('v', 'h', 'l', 'l'); + cm.getSelection('os'); +}, { value: 'cross'}); +testVim('visual_crossover_up', function(cm, vim, helpers) { + cm.setCursor(3, 2); + helpers.doKeys('v', 'j', 'k', 'k'); + eqPos(Pos(2, 2), cm.getCursor('head')); + eqPos(Pos(3, 3), cm.getCursor('anchor')); + helpers.doKeys('k'); + eqPos(Pos(1, 2), cm.getCursor('head')); + eqPos(Pos(3, 3), cm.getCursor('anchor')); +}, { value: 'cross\ncross\ncross\ncross\ncross\n'}); +testVim('visual_crossover_down', function(cm, vim, helpers) { + cm.setCursor(1, 2); + helpers.doKeys('v', 'k', 'j', 'j'); + eqPos(Pos(2, 3), cm.getCursor('head')); + eqPos(Pos(1, 2), cm.getCursor('anchor')); + helpers.doKeys('j'); + eqPos(Pos(3, 3), cm.getCursor('head')); + eqPos(Pos(1, 2), cm.getCursor('anchor')); +}, { value: 'cross\ncross\ncross\ncross\ncross\n'}); testVim('visual_exit', function(cm, vim, helpers) { helpers.doKeys('', 'l', 'j', 'j', ''); eqPos(cm.getCursor('anchor'), cm.getCursor('head')); @@ -1822,20 +1883,23 @@ testVim('visual_line', function(cm, vim, helpers) { helpers.doKeys('l', 'V', 'l', 'j', 'j', 'd'); eq(' 4\n 5', cm.getValue()); }, { value: ' 1\n 2\n 3\n 4\n 5' }); -testVim('visual_block', function(cm, vim, helpers) { +testVim('visual_block_different_line_lengths', function(cm, vim, helpers) { // test the block selection with lines of different length // i.e. extending the selection // till the end of the longest line. helpers.doKeys('', 'l', 'j', 'j', '6', 'l', 'd'); helpers.doKeys('d', 'd', 'd', 'd'); eq('', cm.getValue()); +}, {value: '1234\n5678\nabcdefg'}); +testVim('visual_block_truncate_on_short_line', function(cm, vim, helpers) { // check for left side selection in case // of moving up to a shorter line. - cm.replaceRange('hello world\n{\nthis is\nsparta!', cm.getCursor()); + cm.replaceRange('', cm.getCursor()); cm.setCursor(3, 4); helpers.doKeys('', 'l', 'k', 'k', 'd'); eq('hello world\n{\ntis\nsa!', cm.getValue()); - cm.replaceRange('12345\n67891\nabcde', {line: 0, ch: 0}, {line: cm.lastLine(), ch: 6}); +}, {value: 'hello world\n{\nthis is\nsparta!'}); +testVim('visual_block_corners', function(cm, vim, helpers) { cm.setCursor(1, 2); helpers.doKeys('', '2', 'l', 'k'); // circle around the anchor @@ -1851,6 +1915,8 @@ testVim('visual_block', function(cm, vim, helpers) { helpers.doKeys('4', 'l'); selections = cm.getSelections(); eq('891cde', selections.join('')); +}, {value: '12345\n67891\nabcde'}); +testVim('visual_block_mode_switch', function(cm, vim, helpers) { // switch between visual modes cm.setCursor(1, 1); // blockwise to characterwise visual @@ -1865,7 +1931,7 @@ testVim('visual_block', function(cm, vim, helpers) { helpers.doKeys('V'); selections = cm.getSelections(); eq('67891\nabcde', selections.join('')); -}, {value: '1234\n5678\nabcdefg'}); +}, {value: '12345\n67891\nabcde'}); testVim('visual_block_crossing_short_line', function(cm, vim, helpers) { // visual block with long and short lines cm.setCursor(0, 3); @@ -1945,12 +2011,15 @@ testVim('reselect_visual_block', function(cm, vim, helpers) { helpers.doKeys('', 'k', 'h', ''); cm.setCursor(2, 1); helpers.doKeys('v', 'l', 'g', 'v'); - helpers.assertCursorAt(0, 1); + eqPos(Pos(1, 2), vim.sel.anchor); + eqPos(Pos(0, 1), vim.sel.head); // Ensure selection is done with visual block mode rather than one // continuous range. eq(cm.getSelections().join(''), '23oo') helpers.doKeys('g', 'v'); - helpers.assertCursorAt(2, 3); + eqPos(Pos(2, 1), vim.sel.anchor); + eqPos(Pos(2, 2), vim.sel.head); + helpers.doKeys(''); // Ensure selection of deleted range cm.setCursor(1, 1); helpers.doKeys('v', '', 'j', 'd', 'g', 'v'); @@ -1984,11 +2053,14 @@ testVim('o_visual', function(cm, vim, helpers) { testVim('o_visual_block', function(cm, vim, helpers) { cm.setCursor(0, 1); helpers.doKeys('','3','j','l','l', 'o'); - helpers.assertCursorAt(0, 1); + eqPos(Pos(3, 3), vim.sel.anchor); + eqPos(Pos(0, 1), vim.sel.head); helpers.doKeys('O'); - helpers.assertCursorAt(0, 4); + eqPos(Pos(3, 1), vim.sel.anchor); + eqPos(Pos(0, 3), vim.sel.head); helpers.doKeys('o'); - helpers.assertCursorAt(3, 1); + eqPos(Pos(0, 3), vim.sel.anchor); + eqPos(Pos(3, 1), vim.sel.head); }, { value: 'abcd\nefgh\nijkl\nmnop'}); testVim('changeCase_visual', function(cm, vim, helpers) { cm.setCursor(0, 0); @@ -2023,7 +2095,9 @@ testVim('changeCase_visual_block', function(cm, vim, helpers) { }, { value: 'abcdef\nghijkl\nmnopq\nfoo'}); testVim('visual_paste', function(cm, vim, helpers) { cm.setCursor(0, 0); - helpers.doKeys('v', 'l', 'l', 'y', 'j', 'v', 'l', 'p'); + helpers.doKeys('v', 'l', 'l', 'y'); + helpers.assertCursorAt(0, 0); + helpers.doKeys('3', 'l', 'j', 'v', 'l', 'p'); helpers.assertCursorAt(1, 5); eq('this is a\nunithitest for visual paste', cm.getValue()); cm.setCursor(0, 0);