Files
Moonchild a680bdc637 Issue #1656 - Part 1: Nuke most vim config lines in the tree.
Since these are just interpreted comments, there's 0 impact on actual code.
This removes all lines that match /* vim: set(.*)tw=80: */ with S&R -- there are
a few others scattered around which will be removed manually in a second part.
2020-09-23 13:55:00 +00:00

620 lines
18 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals localStorage, window, document, NodeFilter */
// Some constants from nsIPrefBranch.idl.
const PREF_INVALID = 0;
const PREF_STRING = 32;
const PREF_INT = 64;
const PREF_BOOL = 128;
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
// We prefix all our local storage items with this.
const PREFIX = "Services.prefs:";
/**
* Create a new preference branch. This object conforms largely to
* nsIPrefBranch and nsIPrefService, though it only implements the
* subset needed by devtools. A preference branch can hold child
* preferences while also holding a preference value itself.
*
* @param {PrefBranch} parent the parent branch, or null for the root
* branch.
* @param {String} name the base name of this branch
* @param {String} fullName the fully-qualified name of this branch
*/
function PrefBranch(parent, name, fullName) {
this._parent = parent;
this._name = name;
this._fullName = fullName;
this._observers = {};
this._children = {};
// Properties used when this branch has a value as well.
this._defaultValue = null;
this._hasUserValue = false;
this._userValue = null;
this._type = PREF_INVALID;
}
PrefBranch.prototype = {
PREF_INVALID: PREF_INVALID,
PREF_STRING: PREF_STRING,
PREF_INT: PREF_INT,
PREF_BOOL: PREF_BOOL,
/** @see nsIPrefBranch.root. */
get root() {
return this._fullName;
},
/** @see nsIPrefBranch.getPrefType. */
getPrefType: function (prefName) {
return this._findPref(prefName)._type;
},
/** @see nsIPrefBranch.getBoolPref. */
getBoolPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref._type !== PREF_BOOL) {
throw new Error(`${prefName} does not have bool type`);
}
return thePref._get();
},
/** @see nsIPrefBranch.setBoolPref. */
setBoolPref: function (prefName, value) {
if (typeof value !== "boolean") {
throw new Error("non-bool passed to setBoolPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref._type !== PREF_BOOL) {
throw new Error(`${prefName} does not have bool type`);
}
thePref._set(value);
},
/** @see nsIPrefBranch.getCharPref. */
getCharPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref._type !== PREF_STRING) {
throw new Error(`${prefName} does not have string type`);
}
return thePref._get();
},
/** @see nsIPrefBranch.setCharPref. */
setCharPref: function (prefName, value) {
if (typeof value !== "string") {
throw new Error("non-string passed to setCharPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref._type !== PREF_STRING) {
throw new Error(`${prefName} does not have string type`);
}
thePref._set(value);
},
/** @see nsIPrefBranch.getIntPref. */
getIntPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref._type !== PREF_INT) {
throw new Error(`${prefName} does not have int type`);
}
return thePref._get();
},
/** @see nsIPrefBranch.setIntPref. */
setIntPref: function (prefName, value) {
if (typeof value !== "number") {
throw new Error("non-number passed to setIntPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref._type !== PREF_INT) {
throw new Error(`${prefName} does not have int type`);
}
thePref._set(value);
},
/** @see nsIPrefBranch.clearUserPref */
clearUserPref: function (prefName) {
let thePref = this._findPref(prefName);
thePref._clearUserValue();
},
/** @see nsIPrefBranch.prefHasUserValue */
prefHasUserValue: function (prefName) {
let thePref = this._findPref(prefName);
return thePref._hasUserValue;
},
/** @see nsIPrefBranch.addObserver */
addObserver: function (domain, observer, holdWeak) {
if (holdWeak) {
throw new Error("shim prefs only supports strong observers");
}
if (!(domain in this._observers)) {
this._observers[domain] = [];
}
this._observers[domain].push(observer);
},
/** @see nsIPrefBranch.removeObserver */
removeObserver: function (domain, observer) {
if (!(domain in this._observers)) {
return;
}
let index = this._observers[domain].indexOf(observer);
if (index >= 0) {
this._observers[domain].splice(index, 1);
}
},
/** @see nsIPrefService.savePrefFile */
savePrefFile: function (file) {
if (file) {
throw new Error("shim prefs only supports null file in savePrefFile");
}
// Nothing to do - this implementation always writes back.
},
/** @see nsIPrefService.getBranch */
getBranch: function (prefRoot) {
if (!prefRoot) {
return this;
}
if (prefRoot.endsWith(".")) {
prefRoot = prefRoot.slice(0, -1);
}
// This is a bit weird since it could erroneously return a pref,
// not a pref branch.
return this._findPref(prefRoot);
},
/**
* Return this preference's current value.
*
* @return {Any} The current value of this preference. This may
* return a string, a number, or a boolean depending on the
* preference's type.
*/
_get: function () {
if (this._hasUserValue) {
return this._userValue;
}
return this._defaultValue;
},
/**
* Set the preference's value. The new value is assumed to be a
* user value. After setting the value, this function emits a
* change notification.
*
* @param {Any} value the new value
*/
_set: function (value) {
if (!this._hasUserValue || value !== this._userValue) {
this._userValue = value;
this._hasUserValue = true;
this._saveAndNotify();
}
},
/**
* Set the default value for this preference, and emit a
* notification if this results in a visible change.
*
* @param {Any} value the new default value
*/
_setDefault: function (value) {
if (this._defaultValue !== value) {
this._defaultValue = value;
if (!this._hasUserValue) {
this._saveAndNotify();
}
}
},
/**
* If this preference has a user value, clear it. If a change was
* made, emit a change notification.
*/
_clearUserValue: function () {
if (this._hasUserValue) {
this._userValue = null;
this._hasUserValue = false;
this._saveAndNotify();
}
},
/**
* Helper function to write the preference's value to local storage
* and then emit a change notification.
*/
_saveAndNotify: function () {
let store = {
type: this._type,
defaultValue: this._defaultValue,
hasUserValue: this._hasUserValue,
userValue: this._userValue,
};
localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
this._parent._notify(this._name);
},
/**
* Change this preference's value without writing it back to local
* storage. This is used to handle changes to local storage that
* were made externally.
*
* @param {Number} type one of the PREF_* values
* @param {Any} userValue the user value to use if the pref does not exist
* @param {Any} defaultValue the default value to use if the pref
* does not exist
* @param {Boolean} hasUserValue if a new pref is created, whether
* the default value is also a user value
* @param {Object} store the new value of the preference. It should
* be of the form {type, defaultValue, hasUserValue, userValue};
* where |type| is one of the PREF_* type constants; |defaultValue|
* and |userValue| are the default and user values, respectively;
* and |hasUserValue| is a boolean indicating whether the user value
* is valid
*/
_storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
this._type = type;
this._defaultValue = defaultValue;
this._hasUserValue = hasUserValue;
this._userValue = userValue;
// There's no need to write this back to local storage, since it
// came from there; and this avoids infinite event loops.
this._parent._notify(this._name);
},
/**
* Helper function to find either a Preference or PrefBranch object
* given its name. If the name is not found, throws an exception.
*
* @param {String} prefName the fully-qualified preference name
* @return {Object} Either a Preference or PrefBranch object
*/
_findPref: function (prefName) {
let branchNames = prefName.split(".");
let branch = this;
for (let branchName of branchNames) {
branch = branch._children[branchName];
if (!branch) {
throw new Error("could not find pref branch " + prefName);
}
}
return branch;
},
/**
* Helper function to notify any observers when a preference has
* changed. This will also notify the parent branch for further
* reporting.
*
* @param {String} relativeName the name of the updated pref,
* relative to this branch
*/
_notify: function (relativeName) {
for (let domain in this._observers) {
if (relativeName === domain || domain === "" ||
(domain.endsWith(".") && relativeName.startsWith(domain))) {
// Allow mutation while walking.
let localList = this._observers[domain].slice();
for (let observer of localList) {
try {
observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
relativeName);
} catch (e) {
console.error(e);
}
}
}
}
if (this._parent) {
this._parent._notify(this._name + "." + relativeName);
}
},
/**
* Helper function to create a branch given an array of branch names
* representing the path of the new branch.
*
* @param {Array} branchList an array of strings, one per component
* of the branch to be created
* @return {PrefBranch} the new branch
*/
_createBranch: function (branchList) {
let parent = this;
for (let branch of branchList) {
if (!parent._children[branch]) {
let isParentRoot = !parent.parent;
let branchName = (isParentRoot ? "" : parent.root + ".") + branch;
parent._children[branch] = new PrefBranch(parent, branch, branchName);
}
parent = parent._children[branch];
}
return parent;
},
/**
* Create a new preference. The new preference is assumed to be in
* local storage already, and the new value is taken from there.
*
* @param {String} keyName the full-qualified name of the preference.
* This is also the name of the key in local storage.
* @param {Any} userValue the user value to use if the pref does not exist
* @param {Boolean} hasUserValue if a new pref is created, whether
* the default value is also a user value
* @param {Any} defaultValue the default value to use if the pref
* does not exist
* @param {Boolean} init if true, then this call is initialization
* from local storage and should override the default prefs
*/
_findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue,
init = false) {
let branch = this._createBranch(keyName.split("."));
if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
throw new Error("inconsistent values when creating " + keyName);
}
let type;
switch (typeof (defaultValue)) {
case "boolean":
type = PREF_BOOL;
break;
case "number":
type = PREF_INT;
break;
case "string":
type = PREF_STRING;
break;
default:
throw new Error("unhandled argument type: " + typeof (defaultValue));
}
if (init || branch._type === PREF_INVALID) {
branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
} else if (branch._type !== type) {
throw new Error("attempt to change type of pref " + keyName);
}
return branch;
},
/**
* Helper function that is called when local storage changes. This
* updates the preferences and notifies pref observers as needed.
*
* @param {StorageEvent} event the event representing the local
* storage change
*/
_onStorageChange: function (event) {
if (event.storageArea !== localStorage) {
return;
}
// Ignore delete events. Not clear what's correct.
if (event.key === null || event.newValue === null) {
return;
}
let {type, userValue, hasUserValue, defaultValue} =
JSON.parse(event.newValue);
if (event.oldValue === null) {
this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
} else {
let thePref = this._findPref(event.key);
thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
}
},
/**
* Helper function to initialize the root PrefBranch.
*/
_initializeRoot: function () {
if (Services._defaultPrefsEnabled) {
/* eslint-disable no-eval */
let devtools = require("raw!prefs!devtools/client/preferences/devtools");
eval(devtools);
let all = require("raw!prefs!modules/libpref/init/all");
eval(all);
/* eslint-enable no-eval */
}
// Read the prefs from local storage and create the local
// representations.
for (let i = 0; i < localStorage.length; ++i) {
let keyName = localStorage.key(i);
if (keyName.startsWith(PREFIX)) {
let {userValue, hasUserValue, defaultValue} =
JSON.parse(localStorage.getItem(keyName));
this._findOrCreatePref(keyName.slice(PREFIX.length), userValue,
hasUserValue, defaultValue, true);
}
}
this._onStorageChange = this._onStorageChange.bind(this);
window.addEventListener("storage", this._onStorageChange);
},
};
const Services = {
_prefs: null,
// For use by tests. If set to false before Services.prefs is used,
// this will disable the reading of the default prefs.
_defaultPrefsEnabled: true,
/**
* An implementation of nsIPrefService that is based on local
* storage. Only the subset of nsIPrefService that is actually used
* by devtools is implemented here. This is lazily instantiated so
* that the tests have a chance to disable the loading of default
* prefs.
*/
get prefs() {
if (!this._prefs) {
this._prefs = new PrefBranch(null, "", "");
this._prefs._initializeRoot();
}
return this._prefs;
},
/**
* An implementation of Services.appinfo that holds just the
* properties needed by devtools.
*/
appinfo: {
get OS() {
const os = window.navigator.userAgent;
if (os) {
if (os.includes("Linux")) {
return "Linux";
} else if (os.includes("Windows")) {
return "WINNT";
} else if (os.includes("Mac")) {
return "Darwin";
}
}
return "Unknown";
},
// It's fine for this to be an approximation.
get name() {
return window.navigator.userAgent;
},
// It's fine for this to be an approximation.
get version() {
return window.navigator.appVersion;
},
// This is only used by telemetry, which is disabled for the
// content case. So, being totally wrong is ok.
get is64Bit() {
return true;
},
},
/**
* A no-op implementation of Services.telemetry. This supports just
* the subset of Services.telemetry that is used by devtools.
*/
telemetry: {
getHistogramById: function (name) {
return {
add: () => {}
};
},
getKeyedHistogramById: function (name) {
return {
add: () => {}
};
},
},
/**
* An implementation of Services.focus that holds just the
* properties and methods needed by devtools.
* @see nsIFocusManager.idl for details.
*/
focus: {
// These values match nsIFocusManager in order to make testing a
// bit simpler.
MOVEFOCUS_FORWARD: 1,
MOVEFOCUS_BACKWARD: 2,
get focusedElement() {
if (!document.hasFocus()) {
return null;
}
return document.activeElement;
},
moveFocus: function (window, startElement, type, flags) {
if (flags !== 0) {
throw new Error("shim Services.focus.moveFocus only accepts flags===0");
}
if (type !== Services.focus.MOVEFOCUS_FORWARD
&& type !== Services.focus.MOVEFOCUS_BACKWARD) {
throw new Error("shim Services.focus.moveFocus only supports " +
" MOVEFOCUS_FORWARD and MOVEFOCUS_BACKWARD");
}
if (!startElement) {
startElement = document.activeElement || document;
}
let iter = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, {
acceptNode: function (node) {
let tabIndex = node.getAttribute("tabindex");
if (tabIndex === "-1") {
return NodeFilter.FILTER_SKIP;
}
node.focus();
if (document.activeElement == node) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
});
iter.currentNode = startElement;
// Sets the focus via side effect in the filter.
if (type === Services.focus.MOVEFOCUS_FORWARD) {
iter.nextNode();
} else {
iter.previousNode();
}
},
},
/**
* An implementation of Services.wm that provides a shim for
* getMostRecentWindow.
*/
wm: {
getMostRecentWindow: function () {
// Having the returned object implement openUILinkIn is
// sufficient for our purposes.
return {
openUILinkIn: function (url) {
window.open(url, "_blank");
},
};
},
},
};
/**
* Create a new preference. This is used during startup (see
* devtools/client/preferences/devtools.js) to install the
* default preferences.
*
* @param {String} name the name of the preference
* @param {Any} value the default value of the preference
*/
function pref(name, value) {
let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
thePref._setDefault(value);
}
module.exports = Services;
// This is exported to silence eslint.
exports.pref = pref;