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) {