From 8daf190b2ecc88ce35840cb98f25dac51dd25499 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 28 Oct 2014 23:22:09 +0400 Subject: [PATCH] add CodeMirror api proxy for vim mode --- lib/ace/editor.js | 4 +- lib/ace/keyboard/vim2.js | 922 ++++++++++++++++++++++++++++++++++ lib/ace/keyboard/vim2_test.js | 106 +++- lib/ace/multi_select.js | 2 +- lib/ace/test/all_browser.js | 1 + 5 files changed, 1028 insertions(+), 7 deletions(-) diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 9d911410..13aa2f60 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -158,7 +158,7 @@ var Editor = function(renderer, session) { if (this.curOp) { if (e && e.returnValue === false) return this.curOp = null; - + this._signal("beforeEndOperation"); var command = this.curOp.command; if (command && command.scrollIntoView) { switch (command.scrollIntoView) { @@ -2476,6 +2476,7 @@ 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); @@ -2487,6 +2488,7 @@ 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/vim2.js b/lib/ace/keyboard/vim2.js index 27f18c6e..c0f780ac 100644 --- a/lib/ace/keyboard/vim2.js +++ b/lib/ace/keyboard/vim2.js @@ -62,6 +62,799 @@ define(function(require, exports, module) { 'use strict'; + /* function log() { + var d = ""; + function format(p) { + if (typeof p != "object") + return p + "" + if ("line" in p) { + return p.line + ":" + p.ch + } + if ("anchor" in p) { + return format(p.anchor) + "->" + format(p.head) + } + if (Array.isArray(p)) + return "[" + p.map(function(x) {return format(x)})+"]" + return JSON.stringify(p) + } + for (var i = 0; i < arguments.length; i++) { + var p = arguments[i] + var f = format(p) + d+= f+" " + } + console.log(d) + } */ + var Range = require("../range").Range; + var EventEmitter = require("../lib/event_emitter").EventEmitter; + var dom = require("../lib/dom"); + var oop = require("../lib/oop"); + var KEYS = require("../lib/keys"); + var event = require("../lib/event"); + var Search = require("../search").Search; + var SearchHighlight = require("../search_highlight").SearchHighlight; + var multiSelectCommands = require("../commands/multi_select_commands"); + require("../multi_select"); + + var CodeMirror = function(ace) { + this.ace = ace; + this.state = {}; + this.marks = {}; + this.$uid = 0; + this.onChange = this.onChange.bind(this); + this.onSelectionChange = this.onSelectionChange.bind(this); + this.onBeforeEndOperation = this.onBeforeEndOperation.bind(this); + this.ace.on('change', this.onChange); + this.ace.on('changeSelection', this.onSelectionChange); + this.ace.on('beforeEndOperation', this.onBeforeEndOperation); + }; + CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + CodeMirror.defineOption = function(name, val, setter) {}; + CodeMirror.commands = { + redo: function(cm) { cm.ace.redo(); }, + undo: function(cm) { cm.ace.undo(); }, + newlineAndIndent: function(cm) { cm.ace.insert("\n"); }, + }; + CodeMirror.keyMap = {}; + CodeMirror.addClass = CodeMirror.rmClass = + CodeMirror.e_stop = function() {}; + CodeMirror.keyName = function(e) { + if (e.key) return e.key; + var key = (KEYS[e.keyCode] || ""); + if (key.length == 1) key = key.toUpperCase(); + key = event.getModifierString(e).replace(/(^|-)\w/g, function(m) { + return m.toUpperCase(); + }) + key; + return key; + }; + CodeMirror.keyMap['default'] = function(key) { + return function(cm) { + var cmd = cm.ace.commands.commandKeyBinding[key.toLowerCase()]; + return cmd && cm.ace.execCommand(cmd) !== false; + }; + }; + CodeMirror.lookupKey = function lookupKey(key, map, handle) { + if (typeof map == "string") + map = CodeMirror.keyMap[map]; + var found = typeof map == "function" ? map(key) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (!Array.isArray(map.fallthrough)) + return lookupKey(key, map.fallthrough, handle); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle); + if (result) return result; + } + } + }; + + CodeMirror.signal = function(o, name, e) { return o._signal(name, e) }; + CodeMirror.on = event.addListener; + CodeMirror.off = event.removeListener; +(function() { + oop.implement(CodeMirror.prototype, EventEmitter); + + this.destroy = function() { + this.ace.off('change', this.onChange); + this.ace.off('changeSelection', this.onSelectionChange); + this.ace.off('beforeEndOperation', this.onBeforeEndOperation); + this.removeOverlay(); + }; + this.virtualSelectionMode = function() { + return this.ace.inVirtualSelectionMode && this.ace.selection.index + }; + this.onChange = function(delta) { + var oldDelta = delta.data; + delta = { + start: oldDelta.range.start, + end: oldDelta.range.end, + action: oldDelta.action, + lines: oldDelta.lines || [oldDelta.text] + };// v1.2 api compatibility + if (delta.action[0] == 'i') { + var change = { text: delta.lines }; + var curOp = this.curOp = this.curOp || {}; + if (!curOp.changeHandlers) + curOp.changeHandlers = this._eventRegistry["change"] && this._eventRegistry["change"].slice(); + if (this.virtualSelectionMode()) return; + if (!curOp.lastChange) { + curOp.lastChange = curOp.change = change; + } else { + curOp.lastChange.next = curOp.lastChange = change; + } + } + this.$updateMarkers(delta); + }; + this.onSelectionChange = function() { + var curOp = this.curOp = this.curOp || {}; + if (!curOp.cursorActivityHandlers) + curOp.cursorActivityHandlers = this._eventRegistry["cursorActivity"] && this._eventRegistry["cursorActivity"].slice(); + this.curOp.cursorActivity = true; + if (this.ace.inMultiSelectMode) { + this.ace.keyBinding.removeKeyboardHandler(multiSelectCommands.keyboardHandler); + } + }; + this.operation = function(fn, force) { + if (!force && this.curOp || force && this.curOp && this.curOp.force) { + return fn(); + } + if (force || !this.ace.curOp) { + if (this.curOp) + this.onBeforeEndOperation(); + } + if (!this.ace.curOp) { + var prevOp = this.ace.prevOp; + this.ace.startOperation({ + command: { name: "vim", scrollIntoView: "cursor" } + }); + } + var curOp = this.curOp = this.curOp || {}; + this.curOp.force = force; + var result = fn(); + if (this.ace.curOp && this.ace.curOp.command.name == "vim") { + this.ace.endOperation(); + if (!curOp.cursorActivity && !curOp.lastChange && prevOp) + this.ace.prevOp = prevOp; + } + if (force || !this.ace.curOp) { + if (this.curOp) + this.onBeforeEndOperation(); + } + return result; + }; + this.onBeforeEndOperation = function() { + var op = this.curOp; + if (op) { + if (op.change) { this.signal("change", op.change, op); } + if (op && op.cursorActivity) { this.signal("cursorActivity", null, op); } + this.curOp = null; + } + }; + + this.signal = function(eventName, e, handlers) { + var listeners = handlers ? handlers[eventName + "Handlers"] + : (this._eventRegistry || {})[eventName]; + if (!listeners) + return; + listeners = listeners.slice(); + for (var i=0; i 0) { + point.row += rowShift; + point.column += point.row == end.row ? colShift : 0; + continue; + } + if (!isInsert && cmp2 <= 0) { + point.row = start.row; + point.column = start.column; + if (cmp2 === 0) + point.bias = 1 + } + } + }; + var Marker = function(cm, id, row, column) { + this.cm = cm; + this.id = id; + this.row = row; + this.column = column; + cm.marks[this.id] = this; + }; + Marker.prototype.clear = function() { delete this.cm.marks[this.id] }; + Marker.prototype.find = function() { return toCmPos(this) }; + this.setBookmark = function(cursor, options) { + var bm = new Marker(this, this.$uid++, cursor.line, cursor.ch); + if (!options || !options.insertLeft) + bm.$insertRight = true; + this.marks[bm.id] = bm; + return bm; + }; + this.moveH = function(increment, unit) { + if (unit == 'char') { + var sel = this.ace.selection; + sel.clearSelection(); + sel.moveCursorBy(0, increment); + } + }; + this.findPosV = function(start, amaount, unit, goalColumn) { + if (unit == 'line') { + var screenPos = this.ace.session.documentToScreenPosition(start.line, start.ch); + if (goalColumn != null) + screenPos.column = goalColumn; + screenPos.row += amaount; + // not what codemirror does but vim mode needs only it + screenPos.row = Math.min(Math.max(0, screenPos.row), this.ace.session.getScreenLength() - 1); + var pos = this.ace.session.screenToDocumentPosition(screenPos.row, screenPos.column); + return toCmPos(pos); + } else { + debugger; + } + }; + this.charCoords = function(pos, mode) { + if (mode == 'div' || !mode) { + var sc = this.ace.session.documentToScreenPosition(pos.line, pos.ch); + return {left: sc.column, top: sc.row}; + }if (mode == 'local') { + var renderer = this.ace.renderer; + var sc = this.ace.session.documentToScreenPosition(pos.line, pos.ch); + var lh = renderer.layerConfig.lineHeight; + var cw = renderer.layerConfig.characterWidth; + var top = lh * sc.row; + return {left: sc.column * cw, top: top, bottom: top + lh}; + } + }; + this.coordsChar = function(pos, mode) { + var renderer = this.ace.renderer; + if (mode == 'local') { + var row = Math.max(0, Math.floor(pos.top / renderer.lineHeight)); + var col = Math.max(0, Math.floor(pos.left / renderer.characterWidth)); + var ch = renderer.session.screenToDocumentPosition(row, col); + return toCmPos(ch); + } else if (mode == 'div') { + throw "not implemented"; + } + }; + this.openDialog = function() { + debugger + }; + this.getSearchCursor = function(query, pos, caseFold) { + var caseSensitive = false; + var isRegexp = false; + if (query instanceof RegExp && !query.global) { + caseSensitive = !query.ignoreCase; + query = query.source; + isRegexp = true; + } + var search = new Search(); + if (pos.ch == undefined) pos.ch = Number.MAX_VALUE; + var acePos = {row: pos.line, column: pos.ch}; + var cm = this; + var last = null; + return { + findNext: function() { return this.find(false) }, + findPrevious: function() {return this.find(true) }, + find: function(back) { + search.setOptions({ + needle: query, + caseSensitive: caseSensitive, + wrap: false, + backwards: back, + regExp: isRegexp, + start: last || acePos + }); + var range = search.find(cm.ace.session); + if (range && range.isEmpty()) { + if (cm.getLine(range.start.row).length == range.start.column) { + search.$options.start = range; + range = search.find(cm.ace.session); + } + } + last = range; + return last; + }, + from: function() { return last && toCmPos(last.start) }, + to: function() { return last && toCmPos(last.end) }, + replace: function(text) { + if (last) { + last.end = cm.ace.session.doc.replace(last, text); + } + } + }; + }; + this.scrollTo = function(x, y) { + var renderer = this.ace.renderer; + var config = renderer.layerConfig; + var maxHeight = config.maxHeight; + maxHeight -= (renderer.$size.scrollerHeight - renderer.lineHeight) * renderer.$scrollPastEnd; + if (y != null) this.ace.session.setScrollTop(Math.max(0, Math.min(y, maxHeight))); + if (x != null) this.ace.session.setScrollLeft(Math.max(0, Math.min(x, config.width))); + }; + this.scrollInfo = function() { return 0; }; + this.scrollIntoView = function(pos, margin) { + if (pos) + this.ace.renderer.scrollCursorIntoView(toAcePos(pos), null, margin); + }; + this.getLine = function(row) { return this.ace.session.getLine(row) }; + this.getRange = function(s, e) { + return this.ace.session.getTextRange(new Range(s.line, s.ch, e.line, e.ch)); + }; + this.replaceRange = function(text, s, e) { + if (!e) e = s; + return this.ace.session.replace(new Range(s.line, s.ch, e.line, e.ch), text); + }; + this.replaceSelections = function(p) { + var sel = this.ace.selection; + if (this.ace.inVirtualSelectionMode) { + this.ace.session.replace(sel.getRange(), p[0] || ""); + return; + } + sel.inVirtualSelectionMode = true; + var ranges = sel.rangeList.ranges; + if (!ranges.length) ranges = [this.ace.multiSelect.getRange()]; + for (var i = ranges.length; i--;) + this.ace.session.replace(ranges[i], p[i] || ""); + sel.inVirtualSelectionMode = false; + }; + this.getSelection = function() { + return this.ace.getSelectedText(); + }; + this.getSelections = function() { + return this.listSelections().map(function(x) { + return this.getRange(x.anchor, x.head); + }, this); + }; + this.getInputField = function() { + return this.ace.textInput.getElement(); + }; + this.getWrapperElement = function() { + return this.ace.containter; + }; + var optMap = { + indentWithTabs: "useSoftTabs", + indentUnit: "tabSize", + firstLineNumber: "firstLineNumber" + }; + this.setOption = function(name, val) { + this.state[name] = val; + switch (name) { + case 'indentWithTabs': + name = optMap[name]; + val = !val; + break; + default: + name = optMap[name]; + } + if (name) + this.ace.setOption(name, val); + }; + this.getOption = function(name, val) { + var aceOpt = optMap[name]; + if (aceOpt) + val = this.ace.getOption(aceOpt); + switch (name) { + case 'indentWithTabs': + name = optMap[name]; + return !val; + } + return aceOpt ? val : this.state[name]; + }; + this.toggleOverwrite = function(on) { + this.state.overwrite = on; + return this.ace.setOverwrite(on); + }; + this.addOverlay = function(o) { + if (!this.$searchHighlight || !this.$searchHighlight.session) { + var highlight = new SearchHighlight(null, "ace_highlight-marker", "text"); + var marker = this.ace.session.addDynamicMarker(highlight); + highlight.id = marker.id; + highlight.session = this.ace.session; + highlight.destroy = function(o) { + highlight.session.off("change", highlight.updateOnChange); + highlight.session.off("changeEditor", highlight.destroy); + highlight.session.removeMarker(highlight.id); + highlight.session = null; + }; + highlight.updateOnChange = function(delta) { + delta = delta.data.range;// v1.2 api compatibility + var row = delta.start.row; + if (row == delta.end.row) highlight.cache[row] = undefined; + else highlight.cache.splice(row, highlight.cache.length); + } + highlight.session.on("changeEditor", highlight.destroy); + highlight.session.on("change", highlight.updateOnChange); + } + var re = new RegExp(o.query.source, "gmi"); + console.log(re) + this.$searchHighlight = o.highlight = highlight; + this.$searchHighlight.setRegexp(re); + this.ace.renderer.updateBackMarkers(); + }; + this.removeOverlay = function(o) { + if (this.$searchHighlight && this.$searchHighlight.session) { + this.$searchHighlight.destroy(); + } + }; + this.getScrollInfo = function() { + var renderer = this.ace.renderer; + var config = renderer.layerConfig; + return { + left: renderer.scrollLeft, + top: renderer.scrollTop, + height: config.maxHeight, + width: config.width, + clientHeight: config.height, + clientWidth: config.width + }; + }; + this.getValue = function() { + return this.ace.getValue(); + }; + this.setValue = function(v) { + return this.ace.setValue(v); + }; + this.getTokenTypeAt = function(pos) { + var token = this.ace.session.getTokenAt(pos.line, pos.ch); + return token && /comment|string/.test(token.type) ? "string" : ""; + }; + this.findMatchingBracket = function(pos) { + var m = this.ace.session.findMatchingBracket(toAcePos(pos)); + return {to: m && toCmPos(m)}; + }; + this.indentLine = function(line, method) { + if (method === true) + this.ace.session.indentRows(line, line, "\t"); + else if (method === false) + this.ace.session.outdentRows(new Range(line, 0, line, 0)); + }; + this.indexFromPos = function(pos) { + return this.ace.session.doc.positionToIndex(toAcePos(pos)); + }; + this.posFromIndex = function(index) { + return toCmPos(this.ace.session.doc.indexToPosition(index)); + }; + this.focus = function(index) { + return this.ace.focus(); + }; + this.blur = function(index) { + return this.ace.blur(); + }; + this.defaultTextHeight = function(index) { + return this.ace.renderer.layerConfig.lineHeight; + }; + this.scanForBracket = function(pos, dir, _, options) { + var re = options.bracketRegex.source; + if (dir == 1) { + var m = this.ace.session.$findClosingBracket(re.slice(1, 2), toAcePos(pos), /paren|text/); + } else { + var m = this.ace.session.$findOpeningBracket(re.slice(-2, -1), {row: pos.line, column: pos.ch + 1}, /paren|text/); + } + return m && {pos: toCmPos(m)}; + }; + this.refresh = function() { + return this.ace.resize(true); + }; +}).call(CodeMirror.prototype); + function toAcePos(cmPos) { + return {row: cmPos.line, column: cmPos.ch}; + } + function toCmPos(acePos) { + return new Pos(acePos.row, acePos.column); + } + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + throw "not implemented"; + }, + indentation: function() { + throw "not implemented"; + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + +// todo replace with showCommandLine +CodeMirror.defineExtension = function(name, fn) { + CodeMirror.prototype[name] = fn; +}; +dom.importCssString(".normal-mode .ace_cursor{\ + border: 0!important;\ + background-color: red;\ + opacity: 0.5;\ +}.ace_dialog {\ + position: absolute;\ + left: 0; right: 0;\ + background: white;\ + z-index: 15;\ + padding: .1em .8em;\ + overflow: hidden;\ + color: #333;\ +}\ +.ace_dialog-top {\ + border-bottom: 1px solid #eee;\ + top: 0;\ +}\ +.ace_dialog-bottom {\ + border-top: 1px solid #eee;\ + bottom: 0;\ +}\ +.ace_dialog input {\ + border: none;\ + outline: none;\ + background: transparent;\ + width: 20em;\ + color: inherit;\ + font-family: monospace;\ +}", "vimMode"); +(function() { + function dialogDiv(cm, template, bottom) { + var wrap = cm.ace.container; + var dialog; + dialog = wrap.appendChild(document.createElement("div")); + if (bottom) + dialog.className = "ace_dialog ace_dialog-bottom"; + else + dialog.className = "ace_dialog ace_dialog-top"; + + if (typeof template == "string") { + dialog.innerHTML = template; + } else { // Assuming it's a detached DOM element. + dialog.appendChild(template); + } + return dialog; + } + + function closeNotification(cm, newVal) { + if (cm.state.currentNotificationClose) + cm.state.currentNotificationClose(); + cm.state.currentNotificationClose = newVal; + } + + CodeMirror.defineExtension("openDialog", function(template, callback, options) { + if (this.virtualSelectionMode()) return; + if (!options) options = {}; + + closeNotification(this, null); + + var dialog = dialogDiv(this, template, options.bottom); + var closed = false, me = this; + function close(newVal) { + if (typeof newVal == 'string') { + inp.value = newVal; + } else { + if (closed) return; + closed = true; + dialog.parentNode.removeChild(dialog); + me.focus(); + + if (options.onClose) options.onClose(dialog); + } + } + + var inp = dialog.getElementsByTagName("input")[0], button; + if (inp) { + if (options.value) { + inp.value = options.value; + inp.select(); + } + + if (options.onInput) + CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); + if (options.onKeyUp) + CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); + + CodeMirror.on(inp, "keydown", function(e) { + if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } + if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { + inp.blur(); + CodeMirror.e_stop(e); + close(); + } + if (e.keyCode == 13) callback(inp.value); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); + + inp.focus(); + } else if (button = dialog.getElementsByTagName("button")[0]) { + CodeMirror.on(button, "click", function() { + close(); + me.focus(); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); + + button.focus(); + } + return close; + }); + + CodeMirror.defineExtension("openNotification", function(template, options) { + if (this.virtualSelectionMode()) return; + closeNotification(this, close); + var dialog = dialogDiv(this, template, options && options.bottom); + var closed = false, doneTimer; + var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; + + function close() { + if (closed) return; + closed = true; + clearTimeout(doneTimer); + dialog.parentNode.removeChild(dialog); + } + + CodeMirror.on(dialog, 'click', function(e) { + CodeMirror.e_preventDefault(e); + close(); + }); + + if (duration) + doneTimer = setTimeout(close, duration); + + return close; + }); +})(); + + var defaultKeymap = [ // Key to key mapping. This goes first to make it possible to override // existing mappings. @@ -3752,6 +4545,11 @@ define(function(require, exports, module) { } } function getUserVisibleLines(cm) { + var renderer = cm.ace.renderer; + return { + top: renderer.getFirstFullyVisibleRow(), + bottom: renderer.getLastFullyVisibleRow() + } var scrollInfo = cm.getScrollInfo(); var occludeToleranceTop = 6; var occludeToleranceBottom = 10; @@ -4838,4 +5636,128 @@ define(function(require, exports, module) { //}; // Initialize Vim and make it available as an API. CodeMirror.Vim = Vim(); + + Vim = CodeMirror.Vim; + + specialKey = {'return':'CR',backspace:'BS','delete':'Del',esc:'Esc', + left:'Left',right:'Right',up:'Up',down:'Down',space: 'Space', + home:'Home',end:'End',pageup:'PageUp',pagedown:'PageDown', enter: 'CR' + }; + function lookupKey(hashId, key, e) { + if (key.length > 1 && key[0] == "n") { + key = key.replace("numpad", ""); + } + key = specialKey[key] || key; + var name = ''; + if (e.ctrlKey) { name += 'C-'; } + if (e.altKey) { name += 'A-'; } + if (e.shiftKey) { name += 'S-'; } + + name += key; + if (name.length > 1) { name = '<' + name + '>'; } + return name; + } + var handleKey = CodeMirror.Vim.handleKey + CodeMirror.Vim.handleKey = function(cm, key, origin) { + return cm.operation(function() { + return handleKey(cm, key, origin); + }, true); + }; + exports.CodeMirror = CodeMirror; + var getVim = Vim.maybeInitVimState_; + exports.handler = { + cm: null, + drawCursor: function(style, pixelPos, config, sel, session) { + var vim = this.cm.state.vim; + var w = config.characterWidth; + var h = config.lineHeight; + var top = pixelPos.top; + var left = pixelPos.left; + if (!vim.insertMode) { + var isbackwards = !sel.cursor + ? session.selection.isBackwards() || session.selection.isEmpty() + : Range.comparePoints(sel.cursor, sel.start) <= 0 + if (!isbackwards && left > w) + left -= w + } + if (!vim.insertMode && vim.status) { + h = h / 2; + top += h; + } + style.left = left + "px"; + style.top = top + "px"; + style.width = w + "px"; + style.height = h + "px"; + }, + handleKeyboard: function(data, hashId, key, keyCode, e) { + var cm = data.editor.state.cm; + var vim = getVim(cm); + if (keyCode == -1) return; + if (hashId == -1 || hashId & 1 || hashId === 0 && key.length > 1) { + var insertMode = vim.insertMode; + var name = lookupKey(hashId, key, e || {}); + if (vim.status == null) + vim.status = ""; + var isHandled = CodeMirror.Vim.handleKey(cm, name, 'user'); + if (isHandled && vim.status != null) + vim.status += name; + else if (vim.status == null) + vim.status = ""; + cm._signal("changeStatus"); + if (!isHandled && (hashId != -1 || insertMode)) + return; + return {command: "null", passEvent: !isHandled}; + } + }, + attach: function(editor) { + if (!editor.state) editor.state = {}; + var cm = new CodeMirror(editor); + editor.state.cm = cm; + editor.$vimModeHandler = Object.create(this); + editor.$vimModeHandler.cm = editor.state.cm; + var vim = CodeMirror.Vim.maybeInitVimState_(cm); + CodeMirror.keyMap.vim.attach(cm); + vim.status = null; + cm.on('vim-command-done', function() { + vim.status = null; + cm.ace._signal("changeStatus"); + }); + cm.on("changeStatus", function() { + cm.ace.renderer.updateCursor(); + cm.ace._signal("changeStatus"); + }); + cm.on("vim-mode-change", function() { + cm.ace.renderer.setStyle("normal-mode", !vim.insertMode); + cm._signal("changeStatus"); + }); + cm.ace.renderer.setStyle("normal-mode", !vim.insertMode); + editor.renderer.$cursorLayer.drawCursor = this.drawCursor.bind(editor.$vimModeHandler); + }, + detach: function(editor) { + var cm = editor.state.cm; + CodeMirror.keyMap.vim.detach(cm); + cm.destroy(); + editor.state.cm = null; + editor.$vimModeHandler = null; + editor.renderer.$cursorLayer.drawCursor = null; + editor.renderer.setStyle("normal-mode", false); + }, + getStatusText: function(editor) { + var cm = editor.state.cm; + var vim = getVim(cm); + if (vim.insertMode) + return "INSERT"; + var status = ""; + if (vim.visualMode) { + status += "VISUAL"; + if (vim.visualLine) + status += " LINE"; + if (vim.visualBlock) + status += " BLOCK"; + } + if (vim.status) + status += (status ? " " : "") + vim.status; + return status; + } + } }); diff --git a/lib/ace/keyboard/vim2_test.js b/lib/ace/keyboard/vim2_test.js index 7f016c47..b437bcd7 100644 --- a/lib/ace/keyboard/vim2_test.js +++ b/lib/ace/keyboard/vim2_test.js @@ -1,3 +1,90 @@ + +if (typeof process !== "undefined") { + require("amd-loader"); +} + +define(function(require, exports, module) { + +var EditSession = require("./../edit_session").EditSession; +var Editor = require("./../editor").Editor; +var UndoManager = require("./../undomanager").UndoManager; +var MockRenderer = require("./../test/mockrenderer").MockRenderer; +var JavaScriptMode = require("./../mode/javascript").Mode; +var VirtualRenderer = require("./../virtual_renderer").VirtualRenderer; +var assert = require("./../test/assertions"); +var keys = require("./../lib/keys"); +var vim = require("./vim2"); + +var el = document.createElement("div"); +el.style.position = "fixed"; +el.style.left = "20px"; +el.style.top = "30px"; +el.style.width = "500px"; +el.style.height = "300px"; +document.body.appendChild(el); + +if (!el.getBoundingClientRect) + return console.log("Skipping test: This test only runs in the browser"); + +var renderer = new VirtualRenderer(el); +editor = new Editor(renderer);//(new MockRenderer()); +editor.session.setUndoManager(new UndoManager()); +editor.session.setUseWorker(false); +editor.session.setMode(new JavaScriptMode()); +function CodeMirror(place, opts) { + if (opts.value != null) + editor.session.setValue(opts.value); + editor.setOption("wrap", opts.lineWrapping); + editor.setOption("useSoftTabs", !opts.indentWithTabs); + editor.setKeyboardHandler(null); + editor.setKeyboardHandler(vim.handler); + var cm = editor.state.cm; + cm.setOption("tabSize", opts.tabSize || 4); + cm.setOption("indentUnit", opts.indentUnit || 2); + + cm.setSize = function(w, h) { + var changed = false; + if (w && editor.w != w) { + changed = true; + el.style.width = (editor.w = w) + "px"; + } + if (h && editor.h != h) { + changed = true; + el.style.height = (editor.h = h) + "px"; + } + if (changed) + editor.resize(true); + }; + cm.setSize(500, 300); + return cm; +} +for (var key in vim.CodeMirror) + CodeMirror[key] = vim.CodeMirror[key]; +var editor; +var i = 0; +function test(name, fn) { + // if (name != 'vim_search_history') return + // for (i = 0; i < 1000; i++) + // exports["test " + name + i] = fn; // vim_ex_global_confirm + if (i++ < 0 || /- /.test(name)) + exports["test " + name] = function() {}; + else + exports["test " + name] = fn; +} + + +// cm.setBookmark({ch: 5, line: 0}) +// cm.setBookmark({ch: 4, line: 0}) +// 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, phantom; +var Pos = CodeMirror.Pos; +var place = document.createElement("div"); +var eqPos = assert.deepEqual; +var eq = assert.equal; +var is = assert.ok; + + var code = '' + ' wOrd1 (#%\n' + ' word3] \n' + @@ -203,11 +290,11 @@ function testVim(name, run, opts, expectedFail) { successful = true; } finally { cm.openNotification = savedOpenNotification; - if (!successful || verbose) { - place.style.visibility = "visible"; - } else { - place.removeChild(cm.getWrapperElement()); - } + // if (!successful || verbose) { + // place.style.visibility = "visible"; + // } else { + // place.removeChild(cm.getWrapperElement()); + // } } }, expectedFail); }; @@ -2840,6 +2927,7 @@ testVim('HML', function(cm, vim, helpers) { var textHeight = cm.defaultTextHeight(); cm.setSize(600, lines*textHeight); cm.setCursor(120, 0); + cm.refresh(); //ace! helpers.doKeys('H'); helpers.assertCursorAt(86, 2); helpers.doKeys('L'); @@ -2952,10 +3040,12 @@ testVim('scrollMotion', function(cm, vim, helpers){ is(prevScrollInfo.top < cm.getScrollInfo().top); // Jump to the end of the sandbox. cm.setCursor(1000, 0); + cm.refresh(); //ace! prevCursor = cm.getCursor(); // ctrl-e at the bottom of the file should have no effect. helpers.doKeys(''); eq(prevCursor.line, cm.getCursor().line); + cm.refresh(); //ace! prevScrollInfo = cm.getScrollInfo(); helpers.doKeys(''); eq(prevCursor.line - 1, cm.getCursor().line); @@ -3612,3 +3702,9 @@ testVim('beforeSelectionChange', function(cm, vim, helpers) { }, { value: 'abc' }); +}); + + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec(); +} diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index f393012e..04d644ce 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -552,7 +552,7 @@ var Editor = require("./editor").Editor; var pos = anchor == this.multiSelect.anchor ? range.cursor == range.start ? range.end : range.start : range.cursor; - if (!isSamePoint(pos, anchor)) + if (!isSamePoint(this.session.$clipPositionToDocument(pos.row, pos.column), anchor)) this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange()); } }; diff --git a/lib/ace/test/all_browser.js b/lib/ace/test/all_browser.js index 9b5742dd..f05148e9 100644 --- a/lib/ace/test/all_browser.js +++ b/lib/ace/test/all_browser.js @@ -25,6 +25,7 @@ var testNames = [ "ace/keyboard/emacs_test", "ace/keyboard/keybinding_test", "ace/keyboard/vim_test", + "ace/keyboard/vim2_test", "ace/layer/text_test", "ace/lib/event_emitter_test", "ace/mode/coffee/parser_test",