diff --git a/common/content/commandline.js b/common/content/commandline.js index 06144d3b..05a96f23 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -138,7 +138,7 @@ const CommandLine = Module("commandline", { this._currentCommand = null; // save the arguments for the inputMultiline method which are needed in the event handler - this._multilineRegexp = null; + this._multilineEnd = null; this._multilineCallback = null; this._input = {}; @@ -581,11 +581,11 @@ const CommandLine = Module("commandline", { * which matches the given regular expression. Then execute the * callback with that string as a parameter. * - * @param {RegExp} untilRegexp + * @param {string} end * @param {function(string)} callbackFunc */ // FIXME: Buggy, especially when pasting. Shouldn't use a RegExp. - inputMultiline: function inputMultiline(untilRegexp, callbackFunc) { + inputMultiline: function inputMultiline(end, callbackFunc) { // Kludge. let cmd = !this.widgets.command.collapsed && this.command; modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE); @@ -593,7 +593,7 @@ const CommandLine = Module("commandline", { this._echoLine(cmd, this.HL_NORMAL); // save the arguments, they are needed in the event handler onEvent - this._multilineRegexp = untilRegexp; + this._multilineEnd = "\n" + end + "\n"; this._multilineCallback = callbackFunc; this.widgets.multilineInput.collapsed = false; @@ -702,9 +702,10 @@ const CommandLine = Module("commandline", { if (event.type == "keypress") { let key = events.toString(event); if (events.isAcceptKey(key)) { - let text = this.widgets.multilineInput.value.substr(0, this.widgets.multilineInput.selectionStart); - if (text.match(this._multilineRegexp)) { - text = text.replace(this._multilineRegexp, ""); + let text = "\n" + this.widgets.multilineInput.value.substr(0, this.widgets.multilineInput.selectionStart); + let index = text.indexOf(this._multilineEnd); + if (index >= 0) { + text = text.substring(1, index); modes.pop(); this.widgets.multilineInput.collapsed = true; this._multilineCallback.call(this, text); diff --git a/common/content/commands.js b/common/content/commands.js index 30e1d921..ce4b8a4a 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -137,31 +137,13 @@ const Command = Class("Command", { * @deprecated * @param {Object} modifiers Any modifiers to be passed to {@link #action}. */ - execute: function (args, bang, count, modifiers) { - // XXX - bang = !!bang; - count = (count === undefined) ? null : count; + execute: function (args, modifiers) { + let self = this; modifiers = modifiers || {}; - let self = this; - function exec(command) { - // FIXME: Move to parseCommand? - args = this.parseArgs(command); - args.count = count; - args.bang = bang; + dactyl.trapErrors(function exec(command) { this.action(args, modifiers); - } - - if (this.hereDoc) { - let matches = args.match(/(.*)<<\s*(\S+)$/); - if (matches && matches[2]) { - commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"), - function (args) { dactyl.trapErrors(exec, self, matches[1] + "\n" + args); }); - return; - } - } - - dactyl.trapErrors(exec, this, args); + }, this); }, /** @@ -194,6 +176,7 @@ const Command = Class("Command", { argCount: this.argCount, complete: complete, extra: extra, + hereDoc: this.hereDoc, keepQuotes: !!this.keepQuotes, literal: this.literal, options: this.options @@ -460,26 +443,6 @@ const Commands = Module("commands", { return this._exCommands.filter(function (cmd) cmd.user); }, - _subCommands: function (command) { - while (command) { - // FIXME: This parseCommand/commands.get/parseArgs is duplicated too often. - let [count, cmd, bang, args] = commands.parseCommand(command); - command = commands.get(cmd); - if (command) { - try { - args = command.parseArgs(args, null, { count: count, bang: bang }); - yield [command, args]; - if (commands.subCommand == null) - break; - command = args[command.subCommand]; - } - catch (e) { - break; - } - } - } - }, - /** * Returns true if a command invocation contains a URL referring to the * domain 'host'. @@ -563,6 +526,13 @@ const Commands = Module("commands", { */ parseArgs: function (str, params) { function getNextArg(str) { + if (str.substr(0, 2) === "<<" && hereDoc) { + let arg = /^<<(\S*)/.exec(str)[1]; + let count = arg.length + 2; + if (complete) + return [count, "", ""] + return [count, io.readHeredoc(arg), ""]; + } let [count, arg, quote] = Commands.parseArg(str, null, keepQuotes); if (quote == "\\" && !complete) return [,,,"Trailing \\"]; @@ -571,7 +541,7 @@ const Commands = Module("commands", { return [count, arg, quote]; } - var { allowUnknownOptions, argCount, complete, extra, literal, options, keepQuotes } = params; + var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params || {}; if (!options) options = []; @@ -628,6 +598,11 @@ const Commands = Module("commands", { // skip whitespace while (/\s/.test(str[i]) && i < str.length) i++; + if (str[i] == "|") { + args.string = str.slice(0, i); + args.trailing = str.slice(i + 1); + break; + } if (i == str.length && !complete) break; @@ -728,6 +703,11 @@ const Commands = Module("commands", { if (args.length == literal) { if (complete) args.completeArg = args.length; + // Hack. + if (sub.substr(0, 2) === "<<" && hereDoc) + let ([count, arg] = getNextArg(sub)) { + sub = arg + sub.substr(count); + } args.literalArg = sub; args.push(sub); args.quote = null; @@ -757,7 +737,7 @@ const Commands = Module("commands", { break; } - if (complete) { + if (complete && args.trailing == null) { if (args.completeOpt) { let opt = args.completeOpt; let context = complete.fork(opt.names[0], args.completeStart); @@ -811,12 +791,12 @@ const Commands = Module("commands", { str.replace(/\s*".*$/, ""); // 0 - count, 1 - cmd, 2 - special, 3 - args - let matches = str.match(/^[:\s]*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/); + let matches = str.match(/^([:\s]*(\d+|%)?([a-zA-Z]+|!)(!)?\s*)(.*?)?$/); //var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/); if (!matches) return [null, null, null, null]; - let [, count, cmd, special, args] = matches; + let [, spec, count, cmd, special, args] = matches; // parse count if (count) @@ -824,7 +804,42 @@ const Commands = Module("commands", { else count = this.COUNT_NONE; - return [count, cmd, !!special, args || ""]; + return [count, cmd, !!special, args || "", spec.length]; + }, + + parseCommands: function (str, complete) { + do { + let [count, cmd, bang, args, len] = commands.parseCommand(str); + if (cmd == null) + return; + let command = commands.get(cmd); + if (command && complete) { + complete.fork(command.name); + var context = complete.fork("args", len); + } + + if (command && /\w[!\s]/.test(str)) + args = command.parseArgs(args, context, { count: count, bang: bang }); + else + args = commands.parseArgs(args, { extra: { count: count, bang: bang } }); + args.commandName = cmd; + args.commandString = str.substr(0, len) + args.string; + str = args.trailing; + yield [command, args]; + } + while (str); + }, + + _subCommands: function (command) { + let commands = [command]; + while (command = commands.shift()) + for (let [command, args] in this.parseCommands(command)) { + if (command) { + yield [command, args]; + if (command.subCommand && args[command.subCommand]) + commands.push(args[command.subCommand]); + } + } }, /** @property */ @@ -921,8 +936,13 @@ const Commands = Module("commands", { completion.ex = function ex(context) { // if there is no space between the command name and the cursor // then get completions of the command name - let [count, cmd, bang, args] = commands.parseCommand(context.filter); - let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || []; + for (var [command, args] in commands.parseCommands(context.filter, context)) + if (args.trailing) + context.advance(args.commandString.length + 1); + if (!args) + args = { commandString: context.filter }; + + let [, prefix, junk] = args.commandString.match(/^(:*\s*\d*\s*)\w*(.?)/) || []; context.advance(prefix.length); if (!junk) { context.fork("", 0, this, "command"); @@ -931,40 +951,26 @@ const Commands = Module("commands", { // dynamically get completions as specified with the command's completer function context.highlight(); - let command = cmd && commands.get(cmd); if (!command) { - context.highlight(0, cmd && cmd.length, "SPELLCHECK"); + context.highlight(0, args.commandName && args.commandName.length, "SPELLCHECK"); return; } - [prefix] = context.filter.match(/^(?:\w*[\s!])?\s*/); + [prefix] = args.commandString.match(/^(?:\w*[\s!])?\s*/); let cmdContext = context.fork(command.name, prefix.length); - let argContext = context.fork("args", prefix.length); - args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang }); - if (args && !cmdContext.waitingForTab) { - // FIXME: Move to parseCommand - args.count = count; - args.bang = bang; - if (!args.completeOpt && command.completer) { - cmdContext.advance(args.completeStart); - cmdContext.quote = args.quote; - cmdContext.filter = args.completeFilter; - try { - let compObject = command.completer.call(command, cmdContext, args); - - if (isArray(compObject)) // for now at least, let completion functions return arrays instead of objects - compObject = { start: compObject[0], items: compObject[1] }; - if (compObject != null) { - cmdContext.advance(compObject.start); - cmdContext.filterFunc = null; - cmdContext.completions = compObject.items; - } - } - catch (e) { - dactyl.reportError(e); + try { + if (!cmdContext.waitingForTab) { + if (!args.completeOpt && command.completer) { + cmdContext.advance(args.completeStart); + cmdContext.quote = args.quote; + cmdContext.filter = args.completeFilter; + command.completer.call(command, cmdContext, args); } } } + catch (e) { + dactyl.reportError(e); + } }; completion.userCommand = function userCommand(context) { @@ -1183,16 +1189,16 @@ const Commands = Module("commands", { res.list = list; return res; }; - Commands.complQuote = { - '"': ['"', quote("", '\n\t"\\\\'), '"'], - "'": ["'", quote("", "'", { "'": "''" }), "'"], - "": ["", quote("", "\\\\ '\""), ""] - }; Commands.quoteArg = { '"': quote('"', '\n\t"\\\\'), "'": quote("'", "'", { "'": "''" }), - "": quote("", "\\\\\\s'\"") + "": quote("", "|\\\\\\s'\"") + }; + Commands.complQuote = { + '"': ['"', Commands.quoteArg['"'], '"'], + "'": ["'", Commands.quoteArg["'"], "'"], + "": ["", Commands.quoteArg[""], ""] }; Commands.parseBool = function (arg) { diff --git a/common/content/completion.js b/common/content/completion.js index fc87257c..179c77a9 100644 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -576,7 +576,8 @@ const CompletionContext = Class("CompletionContext", { if (typeof completer == "string") completer = self[completer]; let context = CompletionContext(this, name, offset); - this.contextList.push(context); + if (this.contextList.indexOf(context) < 0) + this.contextList.push(context); if (!context.autoComplete && !context.tabPressed && context.editor) context.waitingForTab = true; diff --git a/common/content/dactyl.js b/common/content/dactyl.js index d75a57fe..ee21abab 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -392,25 +392,24 @@ const Dactyl = Module("dactyl", { modifiers = modifiers || {}; let err = null; - let [count, cmd, special, args] = commands.parseCommand(str.replace(/^'(.*)'$/, "$1")); - let command = commands.get(cmd); + for (let [command, args] in commands.parseCommands(str.replace(/^'(.*)'$/, "$1"))) { + if (command === null) { + err = "E492: Not a " + config.name + " command: " + str; + dactyl.focusContent(); + } + else if (command.action === null) + err = "E666: Internal error: command.action === null"; // TODO: need to perform this test? -- djk + else if (args.count != null && !command.count) + err = "E481: No range allowed"; + else if (args.bang && !command.bang) + err = "E477: No ! allowed"; - if (command === null) { - err = "E492: Not a " + config.name + " command: " + str; - dactyl.focusContent(); + dactyl.assert(!err, err); + if (!silent) + commandline.command = str.replace(/^\s*:\s*/, ""); + + command.execute(args, modifiers); } - else if (command.action === null) - err = "E666: Internal error: command.action === null"; // TODO: need to perform this test? -- djk - else if (count != null && !command.count) - err = "E481: No range allowed"; - else if (special && !command.bang) - err = "E477: No ! allowed"; - - dactyl.assert(!err, err); - if (!silent) - commandline.command = str.replace(/^\s*:\s*/, ""); - - command.execute(args, special, count, modifiers); }, /** @@ -1717,7 +1716,7 @@ const Dactyl = Module("dactyl", { } else { try { - dactyl.userEval(args.string); + dactyl.userEval(args[0]); } catch (e) { dactyl.echoerr(e); diff --git a/common/content/io.js b/common/content/io.js index 6142096c..fd45ac6c 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -207,6 +207,15 @@ const IO = Module("io", { return io.File(file); }, + readHeredoc: function (end) { + let args; + commandline.inputMultiline(end, + function (res) { args = res; }); + while (args === undefined) + dactyl.threadYield(true); + return args; + }, + /** * Runs an external program. * @@ -312,6 +321,7 @@ lookup: */ source: function (filename, silent) { let wasSourcing = this.sourcing; + let readHeredoc = this.readHeredoc; defineModule.loadLog.push("sourcing " + filename); let time = Date.now(); try { @@ -355,61 +365,27 @@ lookup: let str = file.read(); let lines = str.split(/\r\n|[\r\n]/); + this.readHeredoc = function (end) { + let res = []; + for (let [i, line] in iter) + if (line === end) + return res.join("\n"); + else + res.push(line); + dactyl.assert(false, "Unexpected end of file waiting for " + end); + }; + function execute(args) { command.execute(args, special, count, { setFrom: file }); } - for (let [i, line] in Iterator(lines)) { - if (heredocEnd) { // we already are in a heredoc - if (heredocEnd.test(line)) { - execute(heredoc); - heredoc = ""; - heredocEnd = null; - } - else - heredoc += line + "\n"; - } - else { - this.sourcing.line = i + 1; - // skip line comments and blank lines - line = line.replace(/\r$/, ""); + let iter = Iterator(lines); + for (let [i, line] in iter) { + this.sourcing.line = i + 1; + // skip line comments and blank lines + line = line.replace(/\r$/, ""); - if (/^\s*(".*)?$/.test(line)) - continue; - - var [count, cmd, special, args] = commands.parseCommand(line); - var command = commands.get(cmd); - - if (!command) { - let lineNumber = i + 1; - - dactyl.echoerr("Error detected while processing " + file.path, commandline.FORCE_MULTILINE); - commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR, commandline.APPEND_TO_MESSAGES); - dactyl.echoerr("E492: Not an editor command: " + line); - } - else { - if (command.name == "finish") - break; - else if (command.hereDoc) { - // check for a heredoc - let matches = args.match(/(.*)<<\s*(\S+)$/); - - if (matches) { - args = matches[1]; - heredocEnd = RegExp("^" + matches[2] + "$", "m"); - if (matches[1]) - heredoc = matches[1] + "\n"; - continue; - } - } - - execute(args); - } - } + if (!/^\s*(".*)?$/.test(line)) + dactyl.execute(line, null, true); } - - // if no heredoc-end delimiter is found before EOF then - // process the heredoc anyway - Vim compatible ;-) - if (heredocEnd) - execute(heredoc); } if (this._scriptNames.indexOf(file.path) == -1) @@ -428,6 +404,7 @@ lookup: finally { defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms"); this.sourcing = wasSourcing; + this.readHeredoc = readHeredoc; } }, diff --git a/common/content/options.js b/common/content/options.js index 7cf4de20..66baedf9 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -434,7 +434,7 @@ const Option = Class("Option", { } while (value.length); return res; }, - quote: function quote(str) Commands.quoteArg[/[\s"'\\,]|^$/.test(str) ? "'" : ""](str), + quote: function quote(str) Commands.quoteArg[/[\s|"'\\,]|^$/.test(str) ? "'" : ""](str), ops: { boolean: function (operator, values, scope, invert) { diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 9638af63..f4ebe472 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -18,6 +18,7 @@ - Backtracks to the first successful match after pressing backspace. - Supports reverse incremental search. + * Multiple ex commands may now be separated by | * IMPORTANT: Plugins are now loaded from the 'plugins/' directory in 'runtimepath' rather than 'plugin/'. * IMPORTANT: 'loadplugins' is not a regexlist option rather than diff --git a/pentadactyl/TODO b/pentadactyl/TODO index 82be9682..2452855b 100644 --- a/pentadactyl/TODO +++ b/pentadactyl/TODO @@ -56,7 +56,6 @@ FEATURES: 8 :redir and 'verbosefile' 8 middleclick in content == p, and if command line is open, paste there the clipboard buffer 8 all search commands should start searching from the top of the visible viewport -8 allow for multiple ex commands separated with | (see #24) 8 / should work as in vim (i.e., save page positions as well as locations in the history list). 8 jump to the next heading with ]h, next image ]i, previous textbox [t and so on