diff --git a/common/content/autocommands.js b/common/content/autocommands.js index d225c713..8aad6061 100644 --- a/common/content/autocommands.js +++ b/common/content/autocommands.js @@ -169,7 +169,7 @@ const AutoCommands = Module("autocommands", { cmd.toString = function toString() "-javascript " + cmd.source; } else { - cmd = function cmd(args) dactyl.execute(commands.replaceTokens(cmd.source, args), null, true, cmd.sourcing); + cmd = function cmd(args) commands.execute(cmd.source, args, false, null, cmd.sourcing); cmd.sourcing = io.sourcing && update({}, io.sourcing); cmd.toString = function toString() cmd.source; } diff --git a/common/content/commands.js b/common/content/commands.js index bf9f8207..bd37d271 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -412,6 +412,64 @@ const Commands = Module("commands", { return res.join(" "); }, + /** + * Executes an Ex command script. + * + * @param {string} string A string containing the commands to execute. + * @param {object} tokens An optional object containing tokens to be + * interpolated into the command string. + * @param {object} args Optional arguments object to be passed to + * command actions. + * @param {object} sourcing An object containing information about + * the file that is being or has been sourced to obtain the + * command string. + */ + execute: function (string, tokens, silent, args, sourcing) { + io.withSavedValues(["readHeredoc", "sourcing"], function () { + this.sourcing = update({}, sourcing); + + args = update({ setFrom: this.file }, args || {}); + + if (tokens) + string = commands.replaceTokens(string, tokens); + + let lines = string.split(/\r\n|[\r\n]/); + + this.readHeredoc = function (end) { + let res = []; + this.sourcing.line++; + while (++i < lines.length) { + if (lines[i] === end) + return res.join("\n"); + res.push(lines[i]); + } + dactyl.assert(false, "Unexpected end of file waiting for " + end); + }; + + for (var i = 0; i < lines.length && !this.sourcing.finished; i++) { + // Deal with editors from Silly OSs. + let line = lines[i].replace(/\r$/, ""); + + this.sourcing.line = sourcing.line + i; + + // Process escaped new lines + while (i < lines.length && /^\s*\\/.test(lines[i + 1])) + line += "\n" + lines[++i].replace(/^\s*\\/, ""); + + try { + dactyl.execute(line, args); + } + catch (e) { + if (!silent) { + dactyl.echoerr("Error detected while processing " + this.sourcing.file); + dactyl.echomsg("line\t" + this.sourcing.line + ":"); + dactyl.reportError(e, true); + } + } + } + }); + }, + /** * Returns the command with matching name. * @@ -595,9 +653,11 @@ const Commands = Module("commands", { outer: while (i < str.length || complete) { - // skip whitespace - while (/\s/.test(str[i]) && i < str.length) - i++; + var argStart = i; + let re = /^\s*/gy; + re.lastIndex = i; + i += re.exec(str)[0].length; + if (str[i] == "|") { args.string = str.slice(0, i); args.trailing = str.slice(i + 1); @@ -700,14 +760,20 @@ const Commands = Module("commands", { complete.highlight(i, sub.length, "SPELLCHECK"); } - if (args.length == literal) { + if (args.length === literal) { if (complete) args.completeArg = args.length; + + let re = /^(?:\s*(?=\n)|\s*)([^]*)/gy; + re.lastIndex = argStart || 0; + sub = re.exec(str)[1]; + // 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; @@ -791,7 +857,7 @@ 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*))((?:.|\n)*?)?$/); //var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/); if (!matches) return []; @@ -997,7 +1063,7 @@ const Commands = Module("commands", { count: this.count && args.count }; - dactyl.execute(commands.replaceTokens(this.replacementText, tokens), null, true, this.sourcing); + commands.execute(this.replacementText, tokens, false, null, this.sourcing); } // TODO: offer completion.ex? @@ -1103,21 +1169,21 @@ const Commands = Module("commands", { completion.ex(context); }, options: [ - { names: ["-bang"], description: "Command may be proceeded by a !" }, - { names: ["-count"], description: "Command may be preceeded by a count" }, + { names: ["-bang", "-b"], description: "Command may be proceeded by a !" }, + { names: ["-count", "-c"], description: "Command may be preceeded by a count" }, { - names: ["-description"], + names: ["-description", "-desc", "-d"], description: "A user-visible description of the command", type: CommandOption.STRING }, { // TODO: "E180: invalid complete value: " + arg - names: ["-complete"], + names: ["-complete", "-C"], description: "The argument completion function", completer: function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))], type: CommandOption.STRING, validator: function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), }, { - names: ["-nargs"], + names: ["-nargs", "-a"], description: "The allowed number of arguments", completer: [["0", "No arguments are allowed (default)"], ["1", "One argument is allowed"], diff --git a/common/content/configbase.js b/common/content/configbase.js index d316ac5b..c4f9f9ee 100644 --- a/common/content/configbase.js +++ b/common/content/configbase.js @@ -153,7 +153,7 @@ const ConfigBase = Class(ModuleBase, { Null color: blue; Number color: blue; Object color: maroon; - String color: green; + String color: green; white-space: pre; Key font-weight: bold; diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 485aee4b..b8e522e9 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -1994,7 +1994,7 @@ const Dactyl = Module("dactyl", { dactyl.execute(init); else { if (rcFile) { - io.source(rcFile.path, true); + io.source(rcFile.path, false); services.get("environment").set("MY_" + config.idName + "RC", rcFile.path); } else @@ -2022,17 +2022,7 @@ const Dactyl = Module("dactyl", { // all gui options to their default values, if they have not been // set before by any RC file for (let option in values(options.needInit)) - // FIXME: - // 'encoding' option should not be set at this timing. - // Probably a wrong value is set into the option, - // if current page's encoding is not UTF-8. - try { - if (option.name != "encoding"); - option.value = option.value; - } - catch (e) { - dactyl.reportError(e); - } + option.initValue(); if (dactyl.commandLineOptions.postCommands) dactyl.commandLineOptions.postCommands.forEach(function (cmd) { diff --git a/common/content/io.js b/common/content/io.js index 305023ed..763f0e05 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -319,105 +319,61 @@ lookup: source: function (filename, silent) { defineModule.loadLog.push("sourcing " + filename); let time = Date.now(); - this.withSavedValues(["readHeredoc", "sourcing"], function () { - try { - var file = io.File(filename); - this.sourcing = { - file: file.path, - line: 0 - }; + try { + var file = io.File(filename); - if (!file.exists() || !file.isReadable() || file.isDirectory()) { - if (!silent) - dactyl.echoerr("E484: Can't open file " + filename.quote()); - return; - } - - dactyl.echomsg("sourcing " + filename.quote(), 2); - - let uri = services.get("io").newFileURI(file); - - // handle pure JavaScript files specially - if (/\.js$/.test(filename)) { - try { - dactyl.loadScript(uri.spec, Script(file)); - dactyl.helpInitialized = false; - } - catch (e) { - if (e.fileName) - try { - e.fileName = e.fileName.replace(/^(chrome|resource):.*? -> /, ""); - if (e.fileName == uri.spec) - e.fileName = filename; - e.echoerr = <>{e.fileName}:{e.lineNumber}: {e} - } - catch (e) {} - throw e; - } - } - else if (/\.css$/.test(filename)) - storage.styles.registerSheet(uri.spec, false, true); - else { - let heredoc = ""; - let heredocEnd = null; // the string which ends the heredoc - let str = file.read(); - let lines = str.split(/\r\n|[\r\n]/); - - this.readHeredoc = function (end) { - let res = []; - try { - io.sourcing.line++; - while (true) - let ([i, line] = iter.next()) { - if (line === end) - return res.join("\n"); - res.push(line); - } - } - catch (e if e instanceof StopIteration) {} - dactyl.assert(false, "Unexpected end of file waiting for " + end); - }; - - let iter = Iterator(lines); - for (let [i, line] in iter) { - if (this.sourcing.finished) - break; - this.sourcing.line = i + 1; - // skip line comments and blank lines - line = line.replace(/\r$/, ""); - - if (!/^\s*(".*)?$/.test(line)) - try { - dactyl.execute(line, { setFrom: file }); - } - catch (e) { - if (!silent) { - dactyl.echoerr("Error detected while processing " + file.path); - dactyl.echomsg("line\t" + this.sourcing.line + ":"); - dactyl.reportError(e, true); - } - } - } - } - - if (this._scriptNames.indexOf(file.path) == -1) - this._scriptNames.push(file.path); - - dactyl.echomsg("finished sourcing " + filename.quote(), 2); - - dactyl.log("Sourced: " + filename, 3); - } - catch (e) { - if (!(e instanceof FailedAssertion)) - dactyl.reportError(e); - let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e); + if (!file.exists() || !file.isReadable() || file.isDirectory()) { if (!silent) - dactyl.echoerr(message); + dactyl.echoerr("E484: Can't open file " + filename.quote()); + return; } - finally { - defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms"); + + dactyl.echomsg("sourcing " + filename.quote(), 2); + + let uri = services.get("io").newFileURI(file); + + // handle pure JavaScript files specially + if (/\.js$/.test(filename)) { + try { + dactyl.loadScript(uri.spec, Script(file)); + dactyl.helpInitialized = false; + } + catch (e) { + if (e.fileName) + try { + e.fileName = e.fileName.replace(/^(chrome|resource):.*? -> /, ""); + if (e.fileName == uri.spec) + e.fileName = filename; + e.echoerr = <>{e.fileName}:{e.lineNumber}: {e} + } + catch (e) {} + throw e; + } } - }); + else if (/\.css$/.test(filename)) + storage.styles.registerSheet(uri.spec, false, true); + else { + commands.execute(file.read(), null, silent, null, + { file: file.path, line: 1 }); + } + + if (this._scriptNames.indexOf(file.path) == -1) + this._scriptNames.push(file.path); + + dactyl.echomsg("finished sourcing " + filename.quote(), 2); + + dactyl.log("Sourced: " + filename, 3); + } + catch (e) { + if (!(e instanceof FailedAssertion)) + dactyl.reportError(e); + let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e); + if (!silent) + dactyl.echoerr(message); + } + finally { + defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms"); + } }, // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is diff --git a/common/content/mappings.js b/common/content/mappings.js index a3ec6461..da98c0c6 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -384,8 +384,8 @@ const Mappings = Module("mappings", { } else if (args["-ex"]) { rhs = ["-ex", rhs]; - action = function action(count) - dactyl.execute(commands.replaceTokens(rhs[1], { count: count }), null, true, action.sourcing); + action = function action(count) commands.execute(rhs[1], { count: count }, + false, null, action.sourcing); action.sourcing = io.sourcing && update({}, io.sourcing); } else { diff --git a/common/content/options.js b/common/content/options.js index 3fe292bf..261a7647 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -71,6 +71,10 @@ const Option = Class("Option", { this.globalValue = this.parseValues(this.defaultValue); }, + initValue: function () { + dactyl.trapErrors(function () this.values = this.values, this); + }, + /** @property {value} The option's global value. @see #scope */ get globalValue() options.store.get(this.name, {}).value, set globalValue(val) { options.store.set(this.name, { value: val, time: Date.now() }); }, @@ -685,8 +689,11 @@ const Options = Module("options", { memoize(this._optionMap, name, function () Option(names, description, type, defaultValue, extraInfo)); for (let alias in values(names.slice(1))) memoize(this._optionMap, alias, closure); - if (extraInfo.setter) - memoize(this.needInit, this.needInit.length, closure); + if (extraInfo.setter && (!extraInfo.scope || extraInfo.scope & Option.SCOPE_GLOBAL)) + if (dactyl.initialized) + closure().initValue(); + else + memoize(this.needInit, this.needInit.length, closure); // quickly access options with options["wildmode"]: this.__defineGetter__(name, function () this._optionMap[name].values); diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index df7b5ff8..ee5dc1bf 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -18,7 +18,12 @@ - Backtracks to the first successful match after pressing backspace. - Supports reverse incremental search. - * Multiple Ex commands may now be separated by | + * Ex command parsing improvements, including: + - Multiple Ex commands may now be separated by | + - Commands can continue over multiple lines in RC files by + prefixing the continues lines with a \ + - The \ character is no longer treated specially within single + quotes, i.e., 'fo\o''bar' ⇒ fo\o'bar * Command-line is now hidden by default. Added C and M to 'guioptions'. * Hint mode improvements, including: @@ -41,8 +46,6 @@ directory in 'runtimepath' rather than 'plugin/'. * IMPORTANT: 'loadplugins' is now a regexlist option rather than a boolean. - * IMPORTANT: Single quotes no longer treat \ specially. - I.e., 'fo\o''bar' ≡ fo\o'bar * IMPORTANT: 'cdpath' and 'runtimepath' no longer treat ",," specially. Use "." instead. * IMPORTANT: Option value quoting has changed. List options will