diff --git a/lib/ace/editor.js b/lib/ace/editor.js
index 1a3eed0a..6ce561bb 100644
--- a/lib/ace/editor.js
+++ b/lib/ace/editor.js
@@ -2684,6 +2684,7 @@ config.defineOptions(Editor.prototype, "editor", {
useSoftTabs: "session",
tabSize: "session",
wrap: "session",
+ indentedSoftWrap: "session",
foldStyle: "session",
mode: "session"
});
diff --git a/lib/ace/ext/themelist.js b/lib/ace/ext/themelist.js
index 4e3f0ad7..2350a2e2 100644
--- a/lib/ace/ext/themelist.js
+++ b/lib/ace/ext/themelist.js
@@ -51,6 +51,7 @@ var themeData = [
["Dreamweaver" ],
["Eclipse" ],
["GitHub" ],
+ ["IPlastic" ],
["Solarized Light"],
["TextMate" ],
["Tomorrow" ],
diff --git a/lib/ace/keyboard/vim.js b/lib/ace/keyboard/vim.js
index 2eae6b28..1b4507b3 100644
--- a/lib/ace/keyboard/vim.js
+++ b/lib/ace/keyboard/vim.js
@@ -654,6 +654,9 @@ define(function(require, exports, module) {
this.refresh = function() {
return this.ace.resize(true);
};
+ this.getMode = function() {
+ return { name : this.getOption("mode") };
+ }
}).call(CodeMirror.prototype);
function toAcePos(cmPos) {
return {row: cmPos.line, column: cmPos.ch};
@@ -1185,18 +1188,30 @@ dom.importCssString(".normal-mode .ace_cursor{\
}
var options = {};
- function defineOption(name, defaultValue, type) {
- if (defaultValue === undefined) { throw Error('defaultValue is required'); }
+ function defineOption(name, defaultValue, type, aliases, callback) {
+ if (defaultValue === undefined && !callback) {
+ throw Error('defaultValue is required unless callback is provided');
+ }
if (!type) { type = 'string'; }
options[name] = {
type: type,
- defaultValue: defaultValue
+ defaultValue: defaultValue,
+ callback: callback
};
- setOption(name, defaultValue);
+ if (aliases) {
+ for (var i = 0; i < aliases.length; i++) {
+ options[aliases[i]] = options[name];
+ }
+ }
+ if (defaultValue) {
+ setOption(name, defaultValue);
+ }
}
- function setOption(name, value, cm) {
+ function setOption(name, value, cm, cfg) {
var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
if (!option) {
throw Error('Unknown option: ' + name);
}
@@ -1208,17 +1223,60 @@ dom.importCssString(".normal-mode .ace_cursor{\
value = true;
}
}
- option.value = option.type == 'boolean' ? !!value : value;
+ if (option.callback) {
+ if (scope !== 'local') {
+ option.callback(value, undefined);
+ }
+ if (scope !== 'global' && cm) {
+ option.callback(value, cm);
+ }
+ } else {
+ if (scope !== 'local') {
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+ if (scope !== 'global' && cm) {
+ cm.state.vim.options[name] = {value: value};
+ }
+ }
}
- function getOption(name) {
+ function getOption(name, cm, cfg) {
var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
if (!option) {
throw Error('Unknown option: ' + name);
}
- return option.value;
+ if (option.callback) {
+ var local = cm && option.callback(undefined, cm);
+ if (scope !== 'global' && local !== undefined) {
+ return local;
+ }
+ if (scope !== 'local') {
+ return option.callback();
+ }
+ return;
+ } else {
+ var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
+ return (local || (scope !== 'local') && option || {}).value;
+ }
}
+ defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
+ // Option is local. Do nothing for global.
+ if (cm === undefined) {
+ return;
+ }
+ // The 'filetype' option proxies to the CodeMirror 'mode' option.
+ if (name === undefined) {
+ var mode = cm.getMode().name;
+ return mode == 'null' ? '' : mode;
+ } else {
+ var mode = name == '' ? 'null' : name;
+ cm.setOption('mode', mode);
+ }
+ });
+
var createCircularJumpList = function() {
var size = 100;
var pointer = -1;
@@ -1371,8 +1429,9 @@ dom.importCssString(".normal-mode .ace_cursor{\
visualBlock: false,
lastSelection: null,
lastPastedText: null,
- sel: {
- }
+ sel: {},
+ // Buffer-local/window-local values of vim options.
+ options: {}
};
}
return cm.state.vim;
@@ -1434,6 +1493,8 @@ dom.importCssString(".normal-mode .ace_cursor{\
// remove user defined key bindings.
exCommandDispatcher.unmap(lhs, ctx);
},
+ // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
+ // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
setOption: setOption,
getOption: getOption,
defineOption: defineOption,
@@ -1985,7 +2046,8 @@ dom.importCssString(".normal-mode .ace_cursor{\
}
function onPromptKeyDown(e, query, close) {
var keyName = CodeMirror.keyName(e);
- if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && query == '')) {
vimGlobalState.searchHistoryController.pushInput(query);
vimGlobalState.searchHistoryController.reset();
updateSearchQuery(cm, originalQuery);
@@ -1995,6 +2057,10 @@ dom.importCssString(".normal-mode .ace_cursor{\
clearInputState(cm);
close();
cm.focus();
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
}
}
switch (command.searchArgs.querySrc) {
@@ -2055,7 +2121,8 @@ dom.importCssString(".normal-mode .ace_cursor{\
}
function onPromptKeyDown(e, input, close) {
var keyName = CodeMirror.keyName(e), up;
- if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && input == '')) {
vimGlobalState.exCommandHistoryController.pushInput(input);
vimGlobalState.exCommandHistoryController.reset();
CodeMirror.e_stop(e);
@@ -2067,6 +2134,10 @@ dom.importCssString(".normal-mode .ace_cursor{\
up = keyName == 'Up' ? true : false;
input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
close(input);
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
} else {
if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
vimGlobalState.exCommandHistoryController.reset();
@@ -2096,8 +2167,8 @@ dom.importCssString(".normal-mode .ace_cursor{\
var registerName = inputState.registerName;
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 origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
+ var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
var oldHead = copyCursor(origHead);
var oldAnchor = copyCursor(origAnchor);
var newHead, newAnchor;
@@ -2672,10 +2743,11 @@ dom.importCssString(".normal-mode .ace_cursor{\
var anchor = ranges[0].anchor,
head = ranges[0].head;
text = cm.getRange(anchor, head);
- if (!isWhiteSpaceString(text)) {
+ var lastState = vim.lastEditInputState || {};
+ if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
// Exclude trailing whitespace if the range is not all whitespace.
var match = (/\s+$/).exec(text);
- if (match) {
+ if (match && lastState.motionArgs && lastState.motionArgs.forward) {
head = offsetCursor(head, 0, - match[0].length);
text = text.slice(0, - match[0].length);
}
@@ -4307,7 +4379,8 @@ dom.importCssString(".normal-mode .ace_cursor{\
function dialog(cm, template, shortText, onClose, options) {
if (cm.openDialog) {
cm.openDialog(template, onClose, { bottom: true, value: options.value,
- onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp, select: options.select });
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
+ selectValueOnOpen: false});
}
else {
onClose(prompt(shortText, ''));
@@ -4662,6 +4735,7 @@ dom.importCssString(".normal-mode .ace_cursor{\
// pair of commands that have a shared prefix, at least one of their
// shortNames must not match the prefix of the other command.
var defaultExCommandMap = [
+ { name: 'colorscheme', shortName: 'colo' },
{ name: 'map' },
{ name: 'imap', shortName: 'im' },
{ name: 'nmap', shortName: 'nm' },
@@ -4670,7 +4744,10 @@ dom.importCssString(".normal-mode .ace_cursor{\
{ name: 'write', shortName: 'w' },
{ name: 'undo', shortName: 'u' },
{ name: 'redo', shortName: 'red' },
- { name: 'set', shortName: 'set' },
+ { name: 'set', shortName: 'se' },
+ { name: 'set', shortName: 'se' },
+ { name: 'setlocal', shortName: 'setl' },
+ { name: 'setglobal', shortName: 'setg' },
{ name: 'sort', shortName: 'sor' },
{ name: 'substitute', shortName: 's', possiblyAsync: true },
{ name: 'nohlsearch', shortName: 'noh' },
@@ -4683,6 +4760,13 @@ dom.importCssString(".normal-mode .ace_cursor{\
};
ExCommandDispatcher.prototype = {
processCommand: function(cm, input, opt_params) {
+ var that = this;
+ cm.operation(function () {
+ cm.curOp.isVimOp = true;
+ that._processCommand(cm, input, opt_params);
+ });
+ },
+ _processCommand: function(cm, input, opt_params) {
var vim = cm.state.vim;
var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
var previousCommand = commandHistoryRegister.toString();
@@ -4895,6 +4979,13 @@ dom.importCssString(".normal-mode .ace_cursor{\
};
var exCommands = {
+ colorscheme: function(cm, params) {
+ if (!params.args || params.args.length < 1) {
+ showConfirm(cm, cm.getOption('theme'));
+ return;
+ }
+ cm.setOption('theme', params.args[0]);
+ },
map: function(cm, params, ctx) {
var mapArgs = params.args;
if (!mapArgs || mapArgs.length < 2) {
@@ -4928,6 +5019,9 @@ dom.importCssString(".normal-mode .ace_cursor{\
},
set: function(cm, params) {
var setArgs = params.args;
+ // Options passed through to the setOption/getOption calls. May be passed in by the
+ // local/global versions of the set command
+ var setCfg = params.setCfg || {};
if (!setArgs || setArgs.length < 1) {
if (cm) {
showConfirm(cm, 'Invalid mapping: ' + params.input);
@@ -4951,24 +5045,35 @@ dom.importCssString(".normal-mode .ace_cursor{\
optionName = optionName.substring(2);
value = false;
}
+
var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
if (optionIsBoolean && value == undefined) {
// Calling set with a boolean option sets it to true.
value = true;
}
- if (!optionIsBoolean && !value || forceGet) {
- var oldValue = getOption(optionName);
- // If no value is provided, then we assume this is a get.
+ // If no value is provided, then we assume this is a get.
+ if (!optionIsBoolean && value === undefined || forceGet) {
+ var oldValue = getOption(optionName, cm, setCfg);
if (oldValue === true || oldValue === false) {
showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
} else {
showConfirm(cm, ' ' + optionName + '=' + oldValue);
}
} else {
- setOption(optionName, value, cm);
+ setOption(optionName, value, cm, setCfg);
}
},
- registers: function(cm,params) {
+ setlocal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'local'};
+ this.set(cm, params);
+ },
+ setglobal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'global'};
+ this.set(cm, params);
+ },
+ registers: function(cm, params) {
var regArgs = params.args;
var registers = vimGlobalState.registerController.registers;
var regInfo = '----------Registers----------
';
@@ -5590,7 +5695,7 @@ dom.importCssString(".normal-mode .ace_cursor{\
}
function updateFakeCursor(cm) {
var vim = cm.state.vim;
- var from = copyCursor(vim.sel.head);
+ var from = clipCursorToContent(cm, copyCursor(vim.sel.head));
var to = offsetCursor(from, 0, 1);
if (vim.fakeCursor) {
vim.fakeCursor.clear();
@@ -5601,7 +5706,7 @@ dom.importCssString(".normal-mode .ace_cursor{\
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) {
+ if (vim.visualMode && !cm.somethingSelected()) {
exitVisualMode(cm, false);
} else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
vim.visualMode = true;
diff --git a/lib/ace/keyboard/vim_test.js b/lib/ace/keyboard/vim_test.js
index 788cfccb..127f0706 100644
--- a/lib/ace/keyboard/vim_test.js
+++ b/lib/ace/keyboard/vim_test.js
@@ -34,6 +34,7 @@ editor.session.setMode(new JavaScriptMode());
function CodeMirror(place, opts) {
if (opts.value != null)
editor.session.setValue(opts.value);
+ editor.setOption("indentedSoftWrap", false);
editor.setOption("wrap", opts.lineWrapping);
editor.setOption("useSoftTabs", !opts.indentWithTabs);
editor.setKeyboardHandler(null);
@@ -58,6 +59,7 @@ function CodeMirror(place, opts) {
cm.setSize(500, 300);
return cm;
}
+CodeMirror.defineMode = function() {}
for (var key in vim.CodeMirror)
CodeMirror[key] = vim.CodeMirror[key];
var editor;
@@ -595,7 +597,7 @@ testVim('{', function(cm, vim, helpers) {
helpers.doKeys('6', '{');
helpers.assertCursorAt(0, 0);
}, { value: 'a\n\nb\nc\n\nd' });
-testVim('paragraph motions', function(cm, vim, helpers) {
+testVim('paragraph_motions', function(cm, vim, helpers) {
cm.setCursor(10, 0);
helpers.doKeys('{');
helpers.assertCursorAt(4, 0);
@@ -1075,6 +1077,17 @@ testVim('cc_multiply_repeat', function(cm, vim, helpers) {
is(register.linewise);
eq('vim-insert', cm.getOption('keyMap'));
});
+testVim('ct', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', 't', 'w');
+ eq(' word1 word3', cm.getValue());
+ helpers.doKeys('', 'c', '|');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('', '2', 'u', 'w', 'h');
+ helpers.doKeys('c', '2', 'g', 'e');
+ eq(' wordword3', cm.getValue());
+}, { value: ' word1 word2 word3'});
testVim('cc_should_not_append_to_document', function(cm, vim, helpers) {
var expectedLineCount = cm.lineCount();
cm.setCursor(cm.lastLine(), 0);
@@ -1975,7 +1988,11 @@ testVim('visual_block_move_to_eol', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('', 'G', '$');
var selections = cm.getSelections().join();
- eq("123,45,6", selections);
+ eq('123,45,6', selections);
+ // Checks that with cursor at Infinity, finding words backwards still works.
+ helpers.doKeys('2', 'k', 'b');
+ selections = cm.getSelections().join();
+ eq('1', selections);
}, {value: '123\n45\n6'});
testVim('visual_block_different_line_lengths', function(cm, vim, helpers) {
// test the block selection with lines of different length
@@ -2803,6 +2820,44 @@ testVim('exCommand_history', function(cm, vim, helpers) {
onKeyDown({keyCode: keyCodes.Up}, input, close);
eq(input, 'sort');
}, {value: ''});
+testVim('search_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys('/');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
+testVim('exCommand_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
testVim('.', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('2', 'd', 'w');
@@ -3753,17 +3808,111 @@ testVim('set_string', function(cm, vim, helpers) {
eq('c', CodeMirror.Vim.getOption('testoption'));
});
testVim('ex_set_string', function(cm, vim, helpers) {
- CodeMirror.Vim.defineOption('testoption', 'a', 'string');
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string');
// Test default value is set.
- eq('a', CodeMirror.Vim.getOption('testoption'));
+ eq('a', CodeMirror.Vim.getOption('testopt'));
try {
- // Test fail to set 'notestoption'
- helpers.doEx('set notestoption=b');
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
fail();
} catch (expected) {};
// Test setOption
- helpers.doEx('set testoption=c')
- eq('c', CodeMirror.Vim.getOption('testoption'));
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt'));
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+});
+testVim('ex_set_callback', function(cm, vim, helpers) {
+ var global;
+
+ function cb(val, cm, cfg) {
+ if (val === undefined) {
+ // Getter
+ if (cm) {
+ return cm._local;
+ } else {
+ return global;
+ }
+ } else {
+ // Setter
+ if (cm) {
+ cm._local = val;
+ } else {
+ global = val;
+ }
+ }
+ }
+
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string', cb);
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testopt'));
+ try {
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption (Identical to the string tests, but via callback instead)
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+})
+testVim('ex_set_filetype', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ CodeMirror.defineMode('test_mode_2', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ // Test mode is set.
+ helpers.doEx('set filetype=test_mode');
+ eq('test_mode', cm.getMode().name);
+ // Test 'ft' alias also sets mode.
+ helpers.doEx('set ft=test_mode_2');
+ eq('test_mode_2', cm.getMode().name);
+});
+testVim('ex_set_filetype_null', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ cm.setOption('mode', 'test_mode');
+ // Test mode is set to null.
+ helpers.doEx('set filetype=');
+ eq('null', cm.getMode().name);
});
// TODO: Reset key maps after each test.
testVim('ex_map_key2key', function(cm, vim, helpers) {