// Copyright (c) 2006-2009 by Martin Stubenschrott // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. /** @scope modules */ // do NOT create instances of this class yourself, use the helper method // options.add() instead /** * A class representing configuration options. Instances are created by the * {@link Options} class. * * @param {string[]} names The names by which this option is identified. * @param {string} description A short one line description of the option. * @param {string} type The option's value data type (see {@link Option#type}). * @param {string} defaultValue The default value for this option. * @param {Object} extraInfo An optional extra configuration hash. The * following properties are supported. * scope - see {@link Option#scope} * setter - see {@link Option#setter} * getter - see {@link Option#getter} * completer - see {@link Option#completer} * valdator - see {@link Option#validator} * checkHas - see {@link Option#checkHas} * @optional * @private */ const Option = Class("Option", { init: function (names, description, type, defaultValue, extraInfo) { if (!extraInfo) extraInfo = {}; /** @property {string} The option's canonical name. */ this.name = names[0]; /** @property {string[]} All names by which this option is identified. */ this.names = names; /** * @property {string} The option's data type. One of: * "boolean" - Boolean E.g. true * "number" - Integer E.g. 1 * "string" - String E.g. "Vimperator" * "charlist" - Character list E.g. "rb" * "stringlist" - String list E.g. "homepage,quickmark,tabopen,paste" */ this.type = type; /** * @property {number} The scope of the option. This can be local, global, * or both. * @see Options#OPTION_SCOPE_LOCAL * @see Options#OPTION_SCOPE_GLOBAL * @see Options#OPTION_SCOPE_BOTH */ this.scope = (extraInfo.scope & options.OPTION_SCOPE_BOTH) || options.OPTION_SCOPE_GLOBAL; // XXX set to BOTH by default someday? - kstep /** * @property {string} This option's description, as shown in :optionusage. */ this.description = description || ""; /** * @property {value} The option's default value. This value will be used * unless the option is explicitly set either interactively or in an RC * file or plugin. */ this.defaultValue = (defaultValue === undefined) ? null : defaultValue; // "", 0 are valid default values /** * @property {function} The function called when the option value is set. */ this.setter = extraInfo.setter || null; /** * @property {function} The function called when the option value is read. */ this.getter = extraInfo.getter || null; /** * @property {function(CompletionContext, Args)} This option's completer. * @see CompletionContext */ this.completer = extraInfo.completer || null; /** * @property {function} The function called to validate the option's value * when set. */ this.validator = extraInfo.validator || null; /** * @property The function called to determine whether the option already * contains a specified value. * @see #has */ this.checkHas = extraInfo.checkHas || null; /** * @property {boolean} Set to true whenever the option is first set. This * is useful to see whether it was changed from its default value * interactively or by some RC file. */ this.hasChanged = false; /** * @property {nsIFile} The script in which this option was last set. null * implies an interactive command. */ this.setFrom = null; // add no{option} variant of boolean {option} to this.names if (this.type == "boolean") { this.names = []; // reset since order is important for (let [, name] in Iterator(names)) { this.names.push(name); this.names.push("no" + name); } } if (this.globalValue == undefined) this.globalValue = this.defaultValue; }, /** @property {value} The option's global value. @see #scope */ get globalValue() options.store.get(this.name), set globalValue(val) { options.store.set(this.name, val); }, /** * Returns value as an array of parsed values if the option type is * "charlist" or "stringlist" or else unchanged. * * @param {value} value The option value. * @returns {value|string[]} */ parseValues: function (value) { if (this.type == "stringlist") return (value === "") ? [] : value.split(","); if (this.type == "charlist") return Array.slice(value); return value; }, /** * Returns values packed in the appropriate format for the option * type. * * @param {value|string[]} values The option value. * @returns {value} */ joinValues: function (values) { if (this.type == "stringlist") return values.join(","); if (this.type == "charlist") return values.join(""); return values; }, /** @property {value|string[]} The option value or array of values. */ get values() this.parseValues(this.value), set values(values) this.setValues(values, this.scope), /** * Returns the option's value as an array of parsed values if the option * type is "charlist" or "stringlist" or else the simple value. * * @param {number} scope The scope to return these values from (see * {@link Option#scope}). * @returns {value|string[]} */ getValues: function (scope) this.parseValues(this.get(scope)), /** * Sets the option's value from an array of values if the option type is * "charlist" or "stringlist" or else the simple value. * * @param {number} scope The scope to apply these values to (see * {@link Option#scope}). */ setValues: function (values, scope) { this.set(this.joinValues(values), scope || this.scope); }, /** * Returns the value of the option in the specified scope. The * (@link Option#getter) callback, if it exists, is invoked with this value * before it is returned. * * @param {number} scope The scope to return this value from (see * {@link Option#scope}). * @returns {value} */ get: function (scope) { if (scope) { if ((scope & this.scope) == 0) // option doesn't exist in this scope return null; } else scope = this.scope; let aValue; if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL)) aValue = tabs.options[this.name]; if ((scope & options.OPTION_SCOPE_GLOBAL) && (aValue == undefined)) aValue = this.globalValue; if (this.getter) return liberator.trapErrors(this.getter, this, aValue); return aValue; }, /** * Sets the option value to newValue for the specified scope. * The (@link Option#setter) callback, if it exists, is invoked with * newValue. * * @param {value} newValue The option's new value. * @param {number} scope The scope to apply this value to (see * {@link Option#scope}). */ set: function (newValue, scope) { scope = scope || this.scope; if ((scope & this.scope) == 0) // option doesn't exist in this scope return null; if (this.setter) newValue = liberator.trapErrors(this.setter, this, newValue); if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL)) tabs.options[this.name] = newValue; if ((scope & options.OPTION_SCOPE_GLOBAL) && newValue != this.globalValue) this.globalValue = newValue; this.hasChanged = true; }, /** * @property {value} The option's current value. The option's local value, * or if no local value is set, this is equal to the (@link #globalValue). */ get value() this.get(), set value(val) this.set(val), /** * Returns whether the option value contains one or more of the specified * arguments. * * @returns {boolean} */ has: function () { let self = this; let test = function (val) values.indexOf(val) >= 0; if (this.checkHas) test = function (val) values.some(function (value) self.checkHas(value, val)); let values = this.values; // return whether some argument matches return Array.some(arguments, function (val) test(val)); }, /** * Returns whether this option is identified by name. * * @param {string} name * @returns {boolean} */ hasName: function (name) this.names.indexOf(name) >= 0, /** * Returns whether the specified values are valid for this option. * @see Option#validator */ isValidValue: function (values) { if (this.validator) return this.validator(values); else return true; }, /** * Resets the option to its default value. */ reset: function () { this.value = this.defaultValue; }, /** * Sets the option's value using the specified set operator. * * @param {string} operator The set operator. * @param {value|string[]} values The value (or values) to apply. * @param {number} scope The scope to apply this value to (see * {@link #scope}). * @param {boolean} invert Whether this is an invert boolean operation. */ op: function (operator, values, scope, invert) { let newValue = null; let self = this; switch (this.type) { case "boolean": if (operator != "=") break; if (invert) newValue = !this.value; else newValue = values; break; case "number": // TODO: support floats? Validators need updating. if (!/^[+-]?(?:0x[0-9a-f]+|0[0-7]+|0|[1-9]\d*)$/i.test(values)) return "E521: Number required after := " + this.name + "=" + values; let value = parseInt(values/* deduce radix */); switch (operator) { case "+": newValue = this.value + value; break; case "-": newValue = this.value - value; break; case "^": newValue = this.value * value; break; case "=": newValue = value; break; } break; case "charlist": case "stringlist": values = Array.concat(values); switch (operator) { case "+": newValue = util.Array.uniq(Array.concat(this.values, values), true); break; case "^": // NOTE: Vim doesn't prepend if there's a match in the current value newValue = util.Array.uniq(Array.concat(values, this.values), true); break; case "-": newValue = this.values.filter(function (item) values.indexOf(item) == -1); break; case "=": newValue = values; if (invert) { let keepValues = this.values.filter(function (item) values.indexOf(item) == -1); let addValues = values.filter(function (item) self.values.indexOf(item) == -1); newValue = addValues.concat(keepValues); } break; } break; case "string": switch (operator) { case "+": newValue = this.value + values; break; case "-": newValue = this.value.replace(values, ""); break; case "^": newValue = values + this.value; break; case "=": newValue = values; break; } break; default: return "E685: Internal error: option type `" + this.type + "' not supported"; } if (newValue == null) return "Operator " + operator + " not supported for option type " + this.type; if (!this.isValidValue(newValue)) return "E474: Invalid argument: " + values; this.setValues(newValue, scope); } }, { // TODO: Run this by default? /** * Validates the specified values against values generated by the * option's completer function. * * @param {value|string[]} values The value or array of values to validate. * @returns {boolean} */ validateCompleter: function (values) { let context = CompletionContext(""); let res = context.fork("", 0, this, this.completer); if (!res) res = context.allItems.items.map(function (item) [item.text]); return Array.concat(values).every(function (value) res.some(function (item) item[0] == value)); }, }); /** * @instance options */ const Options = Module("options", { requires: ["config", "highlight", "storage"], init: function () { for (let [, pref] in Iterator(this.allPrefs(Options.OLD_SAVED))) { let saved = Options.SAVED + pref.substr(Options.OLD_SAVED.length) if (!this.getPref(saved)) this.setPref(saved, this.getPref(pref)); this.resetPref(pref); } // Host application preferences which need to be changed to work well with // // Work around the popup blocker // TODO: Make this work like safeSetPref var popupAllowedEvents = this._loadPreference("dom.popup_allowed_events", "change click dblclick mouseup reset submit"); if (!/keypress/.test(popupAllowedEvents)) { this._storePreference("dom.popup_allowed_events", popupAllowedEvents + " keypress"); liberator.registerObserver("shutdown", function () { if (this._loadPreference("dom.popup_allowed_events", "") == popupAllowedEvents + " keypress") this._storePreference("dom.popup_allowed_events", popupAllowedEvents); }); } function optionObserver(key, event, option) { // Trigger any setters. let opt = options.get(option); if (event == "change" && opt) opt.set(opt.value, options.OPTION_SCOPE_GLOBAL); } storage.newMap("options", { store: false }); storage.addObserver("options", optionObserver, window); this._optionHash = {}; this._prefContexts = []; this.prefObserver.register(); }, destroy: function () { this.prefObserver.unregister(); }, /** * @property {number} Global option scope. * @final */ OPTION_SCOPE_GLOBAL: 1, /** * @property {number} Local option scope. Options in this scope only * apply to the current tab/buffer. * @final */ OPTION_SCOPE_LOCAL: 2, /** * @property {number} Both local and global option scope. * @final */ OPTION_SCOPE_BOTH: 3, /** @property {Iterator(Option)} @private */ __iterator__: function () { let sorted = [o for ([i, o] in Iterator(this._optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name)); return (v for ([k, v] in Iterator(sorted))); }, /** @property {Object} Observes preference value changes. */ prefObserver: { register: function () { // better way to monitor all changes? this._branch = services.get("pref").getBranch("").QueryInterface(Ci.nsIPrefBranch2); this._branch.addObserver("", this, false); }, unregister: function () { if (this._branch) this._branch.removeObserver("", this); }, observe: function (subject, topic, data) { if (topic != "nsPref:changed") return; // subject is the nsIPrefBranch we're observing (after appropriate QI) // data is the name of the pref that's been changed (relative to subject) switch (data) { case "accessibility.browsewithcaret": let value = options.getPref("accessibility.browsewithcaret", false); liberator.mode = value ? modes.CARET : modes.NORMAL; break; } } }, /** * Adds a new option. * * @param {string[]} names All names for the option. * @param {string} description A description of the option. * @param {string} type The option type (see {@link Option#type}). * @param {value} defaultValue The option's default value. * @param {Object} extra An optional extra configuration hash (see * {@link Map#extraInfo}). * @optional * @returns {boolean} Whether the option was created. */ add: function (names, description, type, defaultValue, extraInfo) { if (!extraInfo) extraInfo = {}; let option = new Option(names, description, type, defaultValue, extraInfo); if (!option) return false; if (option.name in this._optionHash) { // never replace for now liberator.log("Warning: '" + names[0] + "' already exists, NOT replacing existing option.", 1); return false; } // quickly access options with options["wildmode"]: this.__defineGetter__(option.name, function () option.value); this.__defineSetter__(option.name, function (value) { option.value = value; }); this._optionHash[option.name] = option; return true; }, /** * Returns the names of all preferences. * * @param {string} branch The branch in which to search preferences. * @default "" */ allPrefs: function (branch) services.get("pref").getChildList(branch || "", { value: 0 }), /** * Returns the option with name in the specified scope. * * @param {string} name The option's name. * @param {number} scope The option's scope (see {@link Option#scope}). * @optional * @returns {Option} The matching option. */ get: function (name, scope) { if (!scope) scope = options.OPTION_SCOPE_BOTH; if (name in this._optionHash) return (this._optionHash[name].scope & scope) && this._optionHash[name]; for (let opt in Iterator(options)) { try { if (opt.hasName(name)) return (opt.scope & scope) && opt; }catch(e) { liberator.dump(options.__iterator__); liberator.dump(opt); liberator.reportError(e); throw e } } return null; }, /** * Lists all options in scope or only those with changed values * if onlyNonDefault is specified. * * @param {boolean} onlyNonDefault Limit the list to prefs with a * non-default value. * @param {number} scope Only list options in this scope (see * {@link Option#scope}). */ list: function (onlyNonDefault, scope) { if (!scope) scope = options.OPTION_SCOPE_BOTH; function opts(opt) { for (let opt in Iterator(options)) { let option = { isDefault: opt.value == opt.defaultValue, name: opt.name, default: opt.defaultValue, pre: "\u00a0\u00a0", // Unicode nonbreaking space. value: <> }; if (onlyNonDefault && option.isDefault) continue; if (!(opt.scope & scope)) continue; if (opt.type == "boolean") { if (!opt.value) option.pre = "no"; option.default = (option.default ? "" : "no") + opt.name; } else option.value = <>={template.highlight(opt.value)}; yield option; } }; let list = template.options("Options", opts()); commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, /** * Lists all preferences matching filter or only those with * changed values if onlyNonDefault is specified. * * @param {boolean} onlyNonDefault Limit the list to prefs with a * non-default value. * @param {string} filter The list filter. A null filter lists all * prefs. * @optional */ listPrefs: function (onlyNonDefault, filter) { if (!filter) filter = ""; let prefArray = options.allPrefs(); prefArray.sort(); function prefs() { for (let [, pref] in Iterator(prefArray)) { let userValue = services.get("pref").prefHasUserValue(pref); if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1) continue; value = options.getPref(pref); let option = { isDefault: !userValue, default: options._loadPreference(pref, null, true), value: <>={template.highlight(value, true, 100)}, name: pref, pre: "\u00a0\u00a0" // Unicode nonbreaking space. }; yield option; } }; let list = template.options(config.hostApplication + " Options", prefs()); commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, /** * Parses a :set command's argument string. * * @param {string} args The :set command's argument string. * @param {Object} modifiers A hash of parsing modifiers. These are: * scope - see {@link Option#scope} * @optional * @returns {Object} The parsed command object. */ parseOpt: function parseOpt(args, modifiers) { let ret = {}; let matches, prefix, postfix, valueGiven; [matches, prefix, ret.name, postfix, valueGiven, ret.operator, ret.value] = args.match(/^\s*(no|inv)?([a-z_]*)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || []; ret.args = args; ret.onlyNonDefault = false; // used for :set to print non-default options if (!args) { ret.name = "all"; ret.onlyNonDefault = true; } if (matches) ret.option = options.get(ret.name, ret.scope); ret.prefix = prefix; ret.postfix = postfix; ret.all = (ret.name == "all"); ret.get = (ret.all || postfix == "?" || (ret.option && ret.option.type != "boolean" && !valueGiven)); ret.invert = (prefix == "inv" || postfix == "!"); ret.reset = (postfix == "&"); ret.unsetBoolean = (prefix == "no"); ret.scope = modifiers && modifiers.scope; if (!ret.option) return ret; if (ret.value === undefined) ret.value = ""; ret.optionValue = ret.option.get(ret.scope); ret.optionValues = ret.option.getValues(ret.scope); ret.values = ret.option.parseValues(ret.value); return ret; }, /** * Remove the option with matching name. * * @param {string} name The name of the option to remove. This can be * any of the options's names. */ remove: function (name) { for each (let option in this._optionHash) { if (option.hasName(name)) delete this._optionHash[option.name]; } }, /** @property {Object} The options store. */ get store() storage.options, /** * Returns the value of the preference name. * * @param {string} name The preference name. * @param {value} forcedDefault The the default value for this * preference. Used for internal liberator preferences. */ getPref: function (name, forcedDefault) { return this._loadPreference(name, forcedDefault); }, /** * Sets the preference name to value but warns the user * if the value is changed from its default. * * @param {string} name The preference name. * @param {value} value The new preference value. */ // FIXME: Well it used to. I'm looking at you mst! --djk safeSetPref: function (name, value, message) { let val = this._loadPreference(name, null, false); let def = this._loadPreference(name, null, true); let lib = this._loadPreference(Options.SAVED + name); if (lib == null && val != def || val != lib) { let msg = "Warning: setting preference " + name + ", but it's changed from its default value."; if (message) msg += " " + message; liberator.echomsg(msg); } this._storePreference(name, value); this._storePreference(Options.SAVED + name, value); }, /** * Sets the preference name to value. * * @param {string} name The preference name. * @param {value} value The new preference value. */ setPref: function (name, value) { this._storePreference(name, value); }, /** * Resets the preference name to its default value. * * @param {string} name The preference name. */ resetPref: function (name) { try { services.get("pref").clearUserPref(name); } catch (e) { // ignore - thrown if not a user set value } }, /** * Toggles the value of the boolean preference name. * * @param {string} name The preference name. */ invertPref: function (name) { if (services.get("pref").getPrefType(name) == Ci.nsIPrefBranch.PREF_BOOL) this.setPref(name, !this.getPref(name)); else liberator.echoerr("E488: Trailing characters: " + name + "!"); }, /** * Pushes a new preference context onto the context stack. * * @see #withContext */ pushContext: function () { this._prefContexts.push({}); }, /** * Pops the top preference context from the stack. * * @see #withContext */ popContext: function () { for (let [k, v] in Iterator(this._prefContexts.pop())) this._storePreference(k, v); }, /** * Executes func with a new preference context. When func * returns, the context is popped and any preferences set via * {@link #setPref} or {@link #invertPref} are restored to their * previous values. * * @param {function} func The function to call. * @param {Object} func The 'this' object with which to call func * @see #pushContext * @see #popContext */ withContext: function (func, self) { try { this.pushContext(); return func.call(self); } finally { this.popContext(); } }, _storePreference: function (name, value) { if (this._prefContexts.length) { let val = this._loadPreference(name, null); if (val != null) this._prefContexts[this._prefContexts.length - 1][name] = val; } let type = services.get("pref").getPrefType(name); switch (typeof value) { case "string": if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_STRING) { let supportString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); supportString.data = value; services.get("pref").setComplexValue(name, Ci.nsISupportsString, supportString); } else if (type == Ci.nsIPrefBranch.PREF_INT) liberator.echoerr("E521: Number required after =: " + name + "=" + value); else liberator.echoerr("E474: Invalid argument: " + name + "=" + value); break; case "number": if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_INT) services.get("pref").setIntPref(name, value); else liberator.echoerr("E474: Invalid argument: " + name + "=" + value); break; case "boolean": if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_BOOL) services.get("pref").setBoolPref(name, value); else if (type == Ci.nsIPrefBranch.PREF_INT) liberator.echoerr("E521: Number required after =: " + name + "=" + value); else liberator.echoerr("E474: Invalid argument: " + name + "=" + value); break; default: liberator.echoerr("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")"); } }, _loadPreference: function (name, forcedDefault, defaultBranch) { let defaultValue = null; // XXX if (forcedDefault != null) // this argument sets defaults for non-user settable options (like extensions.history.comp_history) defaultValue = forcedDefault; let branch = defaultBranch ? services.get("pref").getDefaultBranch("") : services.get("pref"); let type = services.get("pref").getPrefType(name); try { switch (type) { case Ci.nsIPrefBranch.PREF_STRING: let value = branch.getComplexValue(name, Ci.nsISupportsString).data; // try in case it's a localized string (will throw an exception if not) if (!services.get("pref").prefIsLocked(name) && !services.get("pref").prefHasUserValue(name) && RegExp("chrome://.+/locale/.+\\.properties").test(value)) value = branch.getComplexValue(name, Ci.nsIPrefLocalizedString).data; return value; case Ci.nsIPrefBranch.PREF_INT: return branch.getIntPref(name); case Ci.nsIPrefBranch.PREF_BOOL: return branch.getBoolPref(name); default: return defaultValue; } } catch (e) { return defaultValue; } }, }, { SAVED: "extensions.liberator.saved.", OLD_SAVED: "liberator.saved.", }, { commandline: function () { // TODO: maybe reset in .destroy()? // TODO: move to buffer.js // we have our own typeahead find implementation // See: https://bugzilla.mozilla.org/show_bug.cgi?id=348187 options.safeSetPref("accessibility.typeaheadfind.autostart", false); options.safeSetPref("accessibility.typeaheadfind", false); // actually the above setting should do it, but has no effect in Firefox }, commands: function () { function setAction(args, modifiers) { let bang = args.bang; if (!args.length) args[0] = ""; for (let [, arg] in args) { if (bang) { let onlyNonDefault = false; let reset = false; let invertBoolean = false; if (args[0] == "") { var name = "all"; onlyNonDefault = true; } else { var [matches, name, postfix, valueGiven, operator, value] = arg.match(/^\s*?([a-zA-Z0-9\.\-_{}]+)([?&!])?\s*(([-+^]?)=(.*))?\s*$/); reset = (postfix == "&"); invertBoolean = (postfix == "!"); } if (name == "all" && reset) // TODO: Why? --djk liberator.echoerr("You can't reset all options, it could make " + config.hostApplication + " unusable."); else if (name == "all") options.listPrefs(onlyNonDefault, ""); else if (reset) options.resetPref(name); else if (invertBoolean) options.invertPref(name); else if (valueGiven) { switch (value) { case undefined: value = ""; break; case "true": value = true; break; case "false": value = false; break; default: if (/^\d+$/.test(value)) value = parseInt(value, 10); } options.setPref(name, value); } else options.listPrefs(onlyNonDefault, name); return; } let opt = options.parseOpt(arg, modifiers); liberator.assert(opt, "Error parsing :set command: " + arg); let option = opt.option; liberator.assert(option != null || opt.all, "E518: Unknown option: " + opt.name); // reset a variable to its default value if (opt.reset) { if (opt.all) { for (let option in options) option.reset(); } else { option.setFrom = modifiers.setFrom || null; option.reset(); } } // read access else if (opt.get) { if (opt.all) options.list(opt.onlyNonDefault, opt.scope); else { if (option.type == "boolean") var msg = (opt.optionValue ? " " : "no") + option.name; else msg = " " + option.name + "=" + opt.optionValue; if (options["verbose"] > 0 && option.setFrom) msg += "\n Last set from " + option.setFrom.path; // FIXME: Message highlight group wrapping messes up the indent up for multi-arg verbose :set queries liberator.echo({msg}); } } // write access // NOTE: the behavior is generally Vim compatible but could be // improved. i.e. Vim's behavior is pretty sloppy to no real benefit else { option.setFrom = modifiers.setFrom || null; if (opt.option.type == "boolean") { liberator.assert(!opt.valueGiven, "E474: Invalid argument: " + arg); opt.values = !opt.unsetBoolean; } let res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert); if (res) liberator.echoerr(res); } } } function setCompleter(context, args, modifiers) { let filter = context.filter; if (args.bang) { // list completions for about:config entries if (filter[filter.length - 1] == "=") { context.advance(filter.length); filter = filter.substr(0, filter.length - 1); context.completions = [ [this._loadPreference(filter, null, false), "Current Value"], [this._loadPreference(filter, null, true), "Default Value"] ].filter(function ([k]) k != null); return; } return completion.preference(context); } let opt = options.parseOpt(filter, modifiers); let prefix = opt.prefix; if (context.filter.indexOf("=") == -1) { if (prefix) context.filters.push(function ({ item: opt }) opt.type == "boolean" || prefix == "inv" && opt.values instanceof Array); return completion.option(context, opt.scope); } else if (prefix == "no") return; if (prefix) context.advance(prefix.length); let option = opt.option; context.advance(context.filter.indexOf("=") + 1); if (!option) { context.message = "No such option: " + opt.name; context.highlight(0, name.length, "SPELLCHECK"); } if (opt.get || opt.reset || !option || prefix) return; if (!opt.value) { context.fork("default", 0, this, function (context) { context.title = ["Extra Completions"]; context.completions = [ [option.value, "Current value"], [option.defaultValue, "Default value"] ].filter(function (f) f[0] != ""); }); } context.fork("values", 0, completion, "optionValue", opt.name, opt.operator); } commands.add(["let"], "Set or list a variable", function (args) { args = args.string; if (!args) { let str = { template.map(liberator.globalVariables, function ([i, value]) { let prefix = typeof value == "number" ? "#" : typeof value == "function" ? "*" : " "; return ; }) }
{i} {prefix}{value}
; if (str.*.length()) liberator.echo(str, commandline.FORCE_MULTILINE); else liberator.echomsg("No variables found"); return; } let matches; // 1 - type, 2 - name, 3 - +-., 4 - expr if (matches = args.match(/([$@&])?([\w:]+)\s*([-+.])?=\s*(.+)/)) { if (!matches[1]) { let reference = liberator.variableReference(matches[2]); liberator.assert(reference[0] || !matches[3], "E121: Undefined variable: " + matches[2]); let expr = liberator.evalExpression(matches[4]); liberator.assert(expr !== undefined, "E15: Invalid expression: " + matches[4]); if (!reference[0]) { if (reference[2] == "g") reference[0] = liberator.globalVariables; else return; // for now } if (matches[3]) { if (matches[3] == "+") reference[0][reference[1]] += expr; else if (matches[3] == "-") reference[0][reference[1]] -= expr; else if (matches[3] == ".") reference[0][reference[1]] += expr.toString(); } else { if (!reference[0]) { if (reference[2] == "g") reference[0] = liberator.globalVariables; else return; // for now } if (matches[3]) { if (matches[3] == "+") reference[0][reference[1]] += expr; else if (matches[3] == "-") reference[0][reference[1]] -= expr; else if (matches[3] == ".") reference[0][reference[1]] += expr.toString(); } else reference[0][reference[1]] = expr; } } } // 1 - name else if (matches = args.match(/^\s*([\w:]+)\s*$/)) { let reference = liberator.variableReference(matches[1]); liberator.assert(reference[0], "E121: Undefined variable: " + matches[1]); let value = reference[0][reference[1]]; let prefix = typeof value == "number" ? "#" : typeof value == "function" ? "*" : " "; liberator.echo(reference[1] + "\t\t" + prefix + value); } }, { literal: 0 } ); commands.add(["setl[ocal]"], "Set local option", function (args, modifiers) { modifiers.scope = options.OPTION_SCOPE_LOCAL; setAction(args, modifiers); }, { bang: true, count: true, completer: function (context, args) { return setCompleter(context, args, { scope: options.OPTION_SCOPE_LOCAL }); }, literal: 0 } ); commands.add(["setg[lobal]"], "Set global option", function (args, modifiers) { modifiers.scope = options.OPTION_SCOPE_GLOBAL; setAction(args, modifiers); }, { bang: true, count: true, completer: function (context, args) { return setCompleter(context, args, { scope: options.OPTION_SCOPE_GLOBAL }); }, literal: 0 } ); commands.add(["se[t]"], "Set an option", function (args, modifiers) { setAction(args, modifiers); }, { bang: true, completer: function (context, args) { return setCompleter(context, args); }, serial: function () [ { command: this.name, arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name : opt.name + "=" + opt.value] } for (opt in options) if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & options.OPTION_SCOPE_GLOBAL)) ] }); commands.add(["unl[et]"], "Delete a variable", function (args) { for (let [, name] in args) { let reference = liberator.variableReference(name); if (!reference[0]) { if (!args.bang) liberator.echoerr("E108: No such variable: " + name); return; } delete reference[0][reference[1]]; } }, { argCount: "+", bang: true }); }, completion: function () { completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]); completion.setFunctionCompleter([options.getPref, options.safeSetPref, options.setPref, options.resetPref, options.invertPref], [function () options.allPrefs().map(function (pref) [pref, ""])]); completion.option = function option(context, scope) { context.title = ["Option"]; context.keys = { text: "names", description: "description" }; context.completions = options; if (scope) context.filters.push(function ({ item: opt }) opt.scope & scope); }; completion.optionValue = function (context, name, op, curValue) { let opt = options.get(name); let completer = opt.completer; if (!completer) return; let curValues = curValue != null ? opt.parseValues(curValue) : opt.values; let newValues = opt.parseValues(context.filter); let len = context.filter.length; switch (opt.type) { case "boolean": if (!completer) completer = function () [["true", ""], ["false", ""]]; break; case "stringlist": let target = newValues.pop(); len = target ? target.length : 0; break; case "charlist": len = 0; break; } // TODO: Highlight when invalid context.advance(context.filter.length - len); context.title = ["Option Value"]; let completions = completer(context); if (!completions) return; // Not Vim compatible, but is a significant enough improvement // that it's worth breaking compatibility. if (newValues instanceof Array) { completions = completions.filter(function (val) newValues.indexOf(val[0]) == -1); switch (op) { case "+": completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1); break; case "-": completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1); break; } } context.completions = completions; }; completion.preference = function preference(context) { context.anchored = false; context.title = [config.hostApplication + " Preference", "Value"]; context.keys = { text: function (item) item, description: function (item) options.getPref(item) }; context.completions = options.allPrefs(); }; }, }); // vim: set fdm=marker sw=4 ts=4 et: