зеркало из https://github.com/mozilla/gecko-dev.git
merge fx-team to mozilla-central
This commit is contained in:
Коммит
e64cd11ad7
2
CLOBBER
2
CLOBBER
|
@ -22,4 +22,4 @@
|
|||
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
|
||||
# don't change CLOBBER for WebIDL changes any more.
|
||||
|
||||
Bug 994964 apparently requires a clobber, unclear why
|
||||
Bug 1004726 requires a clobber for B2G Emulator builds
|
||||
|
|
|
@ -5,3 +5,9 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
|
||||
JS_MODULES_PATH = 'modules/sdk'
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'source/app-extension/bootstrap.js',
|
||||
]
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<em:creator>Mozilla Corporation</em:creator>
|
||||
<em:homepageURL></em:homepageURL>
|
||||
<em:optionsType></em:optionsType>
|
||||
<em:optionsURL></em:optionsURL>
|
||||
<em:updateURL></em:updateURL>
|
||||
</Description>
|
||||
</RDF>
|
||||
|
|
|
@ -12,7 +12,7 @@ const { once } = require('../system/events');
|
|||
const { exit, env, staticArgs } = require('../system');
|
||||
const { when: unload } = require('../system/unload');
|
||||
const { loadReason } = require('../self');
|
||||
const { rootURI } = require("@loader/options");
|
||||
const { rootURI, metadata: { preferences } } = require("@loader/options");
|
||||
const globals = require('../system/globals');
|
||||
const xulApp = require('../system/xul-app');
|
||||
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
|
||||
|
@ -129,20 +129,40 @@ function run(options) {
|
|||
catch(error) {
|
||||
console.exception(error);
|
||||
}
|
||||
// Initialize inline options localization, without preventing addon to be
|
||||
// run in case of error
|
||||
try {
|
||||
require('../l10n/prefs');
|
||||
}
|
||||
catch(error) {
|
||||
console.exception(error);
|
||||
}
|
||||
|
||||
// TODO: When bug 564675 is implemented this will no longer be needed
|
||||
// Always set the default prefs, because they disappear on restart
|
||||
if (options.prefsURI) {
|
||||
// Only set if `prefsURI` specified
|
||||
setDefaultPrefs(options.prefsURI);
|
||||
// native-options does stuff directly with preferences key from package.json
|
||||
if (preferences && preferences.length > 0) {
|
||||
try {
|
||||
require('../preferences/native-options').enable(preferences);
|
||||
}
|
||||
catch (error) {
|
||||
console.exception(error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// keeping support for addons packaged with older SDK versions,
|
||||
// when cfx didn't include the 'preferences' key in @loader/options
|
||||
|
||||
// Initialize inline options localization, without preventing addon to be
|
||||
// run in case of error
|
||||
try {
|
||||
require('../l10n/prefs').enable();
|
||||
}
|
||||
catch(error) {
|
||||
console.exception(error);
|
||||
}
|
||||
|
||||
// TODO: When bug 564675 is implemented this will no longer be needed
|
||||
// Always set the default prefs, because they disappear on restart
|
||||
if (options.prefsURI) {
|
||||
// Only set if `prefsURI` specified
|
||||
try {
|
||||
setDefaultPrefs(options.prefsURI);
|
||||
}
|
||||
catch (err) {
|
||||
// cfx bootstrap always passes prefsURI, even in addons without prefs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is where the addon's main.js finally run.
|
||||
|
|
|
@ -9,7 +9,7 @@ module.metadata = {
|
|||
};
|
||||
|
||||
const { on, once, off, setListeners } = require('./core');
|
||||
const { method, chainable } = require('../lang/functional');
|
||||
const { method, chainable } = require('../lang/functional/core');
|
||||
const { Class } = require('../core/heritage');
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,13 +5,22 @@
|
|||
|
||||
const { on } = require("../system/events");
|
||||
const core = require("./core");
|
||||
const { id: jetpackId} = require('../self');
|
||||
const { id: jetpackId } = require('../self');
|
||||
|
||||
const OPTIONS_DISPLAYED = "addon-options-displayed";
|
||||
|
||||
function enable() {
|
||||
on(OPTIONS_DISPLAYED, onOptionsDisplayed);
|
||||
}
|
||||
exports.enable = enable;
|
||||
|
||||
function onOptionsDisplayed({ subject: document, data: addonId }) {
|
||||
if (addonId !== jetpackId)
|
||||
return;
|
||||
localizeInlineOptions(document);
|
||||
}
|
||||
|
||||
function localizeInlineOptions(document) {
|
||||
let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
|
||||
'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
|
||||
let nodes = document.querySelectorAll(query);
|
||||
|
@ -39,4 +48,4 @@ function onOptionsDisplayed({ subject: document, data: addonId }) {
|
|||
}
|
||||
}
|
||||
}
|
||||
on(OPTIONS_DISPLAYED, onOptionsDisplayed);
|
||||
exports.localizeInlineOptions = localizeInlineOptions;
|
||||
|
|
|
@ -12,389 +12,36 @@ module.metadata = {
|
|||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { deprecateFunction } = require("../util/deprecate");
|
||||
const { setImmediate, setTimeout, clearTimeout } = require("../timers");
|
||||
const { defer, remit, delay, debounce,
|
||||
throttle } = require("./functional/concurrent");
|
||||
const { method, invoke, partial, curry, compose, wrap, identity, memoize, once,
|
||||
cache, complement, constant, when, apply, flip, field, query,
|
||||
isInstance, chainable, is, isnt } = require("./functional/core");
|
||||
|
||||
const arity = f => f.arity || f.length;
|
||||
|
||||
const name = f => f.displayName || f.name;
|
||||
|
||||
const derive = (f, source) => {
|
||||
f.displayName = name(source);
|
||||
f.arity = arity(source);
|
||||
return f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes variadic numeber of functions and returns composed one.
|
||||
* Returned function pushes `this` pseudo-variable to the head
|
||||
* of the passed arguments and invokes all the functions from
|
||||
* left to right passing same arguments to them. Composite function
|
||||
* returns return value of the right most funciton.
|
||||
*/
|
||||
const method = (...lambdas) => {
|
||||
return function method(...args) {
|
||||
args.unshift(this);
|
||||
return lambdas.reduce((_, lambda) => lambda.apply(this, args),
|
||||
void(0));
|
||||
};
|
||||
};
|
||||
exports.method = method;
|
||||
|
||||
/**
|
||||
* Takes a function and returns a wrapped one instead, calling which will call
|
||||
* original function in the next turn of event loop. This is basically utility
|
||||
* to do `setImmediate(function() { ... })`, with a difference that returned
|
||||
* function is reused, instead of creating a new one each time. This also allows
|
||||
* to use this functions as event listeners.
|
||||
*/
|
||||
const defer = f => derive(function(...args) {
|
||||
setImmediate(invoke, f, args, this);
|
||||
}, f);
|
||||
exports.defer = defer;
|
||||
// Exporting `remit` alias as `defer` may conflict with promises.
|
||||
exports.remit = defer;
|
||||
|
||||
/**
|
||||
* Invokes `callee` by passing `params` as an arguments and `self` as `this`
|
||||
* pseudo-variable. Returns value that is returned by a callee.
|
||||
* @param {Function} callee
|
||||
* Function to invoke.
|
||||
* @param {Array} params
|
||||
* Arguments to invoke function with.
|
||||
* @param {Object} self
|
||||
* Object to be passed as a `this` pseudo variable.
|
||||
*/
|
||||
const invoke = (callee, params, self) => callee.apply(self, params);
|
||||
exports.invoke = invoke;
|
||||
|
||||
/**
|
||||
* Takes a function and bind values to one or more arguments, returning a new
|
||||
* function of smaller arity.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* The function to partial
|
||||
*
|
||||
* @returns The new function with binded values
|
||||
*/
|
||||
const partial = (f, ...curried) => {
|
||||
if (typeof(f) !== "function")
|
||||
throw new TypeError(String(f) + " is not a function");
|
||||
|
||||
let fn = derive(function(...args) {
|
||||
return f.apply(this, curried.concat(args));
|
||||
}, f);
|
||||
fn.arity = arity(f) - curried.length;
|
||||
return fn;
|
||||
};
|
||||
exports.partial = partial;
|
||||
|
||||
/**
|
||||
* Returns function with implicit currying, which will continue currying until
|
||||
* expected number of argument is collected. Expected number of arguments is
|
||||
* determined by `fn.length`. Using this with variadic functions is stupid,
|
||||
* so don't do it.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* var sum = curry(function(a, b) {
|
||||
* return a + b
|
||||
* })
|
||||
* console.log(sum(2, 2)) // 4
|
||||
* console.log(sum(2)(4)) // 6
|
||||
*/
|
||||
const curry = new function() {
|
||||
const currier = (fn, arity, params) => {
|
||||
// Function either continues to curry arguments or executes function
|
||||
// if desired arguments have being collected.
|
||||
const curried = function(...input) {
|
||||
// Prepend all curried arguments to the given arguments.
|
||||
if (params) input.unshift.apply(input, params);
|
||||
// If expected number of arguments has being collected invoke fn,
|
||||
// othrewise return curried version Otherwise continue curried.
|
||||
return (input.length >= arity) ? fn.apply(this, input) :
|
||||
currier(fn, arity, input);
|
||||
};
|
||||
curried.arity = arity - (params ? params.length : 0);
|
||||
|
||||
return curried;
|
||||
};
|
||||
|
||||
return fn => currier(fn, arity(fn));
|
||||
};
|
||||
exports.curry = curry;
|
||||
|
||||
/**
|
||||
* Returns the composition of a list of functions, where each function consumes
|
||||
* the return value of the function that follows. In math terms, composing the
|
||||
* functions `f()`, `g()`, and `h()` produces `f(g(h()))`.
|
||||
* @example
|
||||
*
|
||||
* var greet = function(name) { return "hi: " + name; };
|
||||
* var exclaim = function(statement) { return statement + "!"; };
|
||||
* var welcome = compose(exclaim, greet);
|
||||
*
|
||||
* welcome('moe'); // => 'hi: moe!'
|
||||
*/
|
||||
function compose(...lambdas) {
|
||||
return function composed(...args) {
|
||||
let index = lambdas.length;
|
||||
while (0 <= --index)
|
||||
args = [lambdas[index].apply(this, args)];
|
||||
|
||||
return args[0];
|
||||
};
|
||||
}
|
||||
exports.compose = compose;
|
||||
|
||||
/*
|
||||
* Returns the first function passed as an argument to the second,
|
||||
* allowing you to adjust arguments, run code before and after, and
|
||||
* conditionally execute the original function.
|
||||
* @example
|
||||
*
|
||||
* var hello = function(name) { return "hello: " + name; };
|
||||
* hello = wrap(hello, function(f) {
|
||||
* return "before, " + f("moe") + ", after";
|
||||
* });
|
||||
*
|
||||
* hello(); // => 'before, hello: moe, after'
|
||||
*/
|
||||
const wrap = (f, wrapper) => derive(function wrapped(...args) {
|
||||
return wrapper.apply(this, [f].concat(args));
|
||||
}, f);
|
||||
exports.wrap = wrap;
|
||||
|
||||
/**
|
||||
* Returns the same value that is used as the argument. In math: f(x) = x
|
||||
*/
|
||||
const identity = value => value;
|
||||
exports.identity = identity;
|
||||
|
||||
/**
|
||||
* Memoizes a given function by caching the computed result. Useful for
|
||||
* speeding up slow-running computations. If passed an optional hashFunction,
|
||||
* it will be used to compute the hash key for storing the result, based on
|
||||
* the arguments to the original function. The default hashFunction just uses
|
||||
* the first argument to the memoized function as the key.
|
||||
*/
|
||||
const memoize = (f, hasher) => {
|
||||
let memo = Object.create(null);
|
||||
let cache = new WeakMap();
|
||||
hasher = hasher || identity;
|
||||
return derive(function memoizer(...args) {
|
||||
const key = hasher.apply(this, args);
|
||||
const type = typeof(key);
|
||||
if (key && (type === "object" || type === "function")) {
|
||||
if (!cache.has(key))
|
||||
cache.set(key, f.apply(this, args));
|
||||
return cache.get(key);
|
||||
}
|
||||
else {
|
||||
if (!(key in memo))
|
||||
memo[key] = f.apply(this, args);
|
||||
return memo[key];
|
||||
}
|
||||
}, f);
|
||||
};
|
||||
exports.memoize = memoize;
|
||||
|
||||
/**
|
||||
* Much like setTimeout, invokes function after wait milliseconds. If you pass
|
||||
* the optional arguments, they will be forwarded on to the function when it is
|
||||
* invoked.
|
||||
*/
|
||||
const delay = function delay(f, ms, ...args) {
|
||||
setTimeout(() => f.apply(this, args), ms);
|
||||
};
|
||||
exports.remit = remit;
|
||||
exports.delay = delay;
|
||||
|
||||
/**
|
||||
* Creates a version of the function that can only be called one time. Repeated
|
||||
* calls to the modified function will have no effect, returning the value from
|
||||
* the original call. Useful for initialization functions, instead of having to
|
||||
* set a boolean flag and then check it later.
|
||||
*/
|
||||
const once = f => {
|
||||
let ran = false, cache;
|
||||
return derive(function(...args) {
|
||||
return ran ? cache : (ran = true, cache = f.apply(this, args));
|
||||
}, f);
|
||||
};
|
||||
exports.once = once;
|
||||
// export cache as once will may be conflicting with event once a lot.
|
||||
exports.cache = once;
|
||||
|
||||
// Takes a `f` function and returns a function that takes the same
|
||||
// arguments as `f`, has the same effects, if any, and returns the
|
||||
// opposite truth value.
|
||||
const complement = f => derive(function(...args) {
|
||||
return args.length < arity(f) ? complement(partial(f, ...args)) :
|
||||
!f.apply(this, args);
|
||||
}, f);
|
||||
exports.complement = complement;
|
||||
|
||||
// Constructs function that returns `x` no matter what is it
|
||||
// invoked with.
|
||||
const constant = x => _ => x;
|
||||
exports.constant = constant;
|
||||
|
||||
// Takes `p` predicate, `consequent` function and an optional
|
||||
// `alternate` function and composes function that returns
|
||||
// application of arguments over `consequent` if application over
|
||||
// `p` is `true` otherwise returns application over `alternate`.
|
||||
// If `alternate` is not a function returns `undefined`.
|
||||
const when = (p, consequent, alternate) => {
|
||||
if (typeof(alternate) !== "function" && alternate !== void(0))
|
||||
throw TypeError("alternate must be a function");
|
||||
if (typeof(consequent) !== "function")
|
||||
throw TypeError("consequent must be a function");
|
||||
|
||||
return function(...args) {
|
||||
return p.apply(this, args) ?
|
||||
consequent.apply(this, args) :
|
||||
alternate && alternate.apply(this, args);
|
||||
};
|
||||
};
|
||||
exports.when = when;
|
||||
|
||||
// Apply function that behaves as `apply` does in lisp:
|
||||
// apply(f, x, [y, z]) => f.apply(f, [x, y, z])
|
||||
// apply(f, x) => f.apply(f, [x])
|
||||
const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop()));
|
||||
exports.apply = apply;
|
||||
|
||||
// Returns function identical to given `f` but with flipped order
|
||||
// of arguments.
|
||||
const flip = f => derive(function(...args) {
|
||||
return f.apply(this, args.reverse());
|
||||
}, f);
|
||||
exports.flip = flip;
|
||||
|
||||
|
||||
// Takes field `name` and `target` and returns value of that field.
|
||||
// If `target` is `null` or `undefined` it would be returned back
|
||||
// instead of attempt to access it's field. Function is implicitly
|
||||
// curried, this allows accessor function generation by calling it
|
||||
// with only `name` argument.
|
||||
const field = curry((name, target) =>
|
||||
// Note: Permisive `==` is intentional.
|
||||
target == null ? target : target[name]);
|
||||
exports.field = field;
|
||||
|
||||
// Takes `.` delimited string representing `path` to a nested field
|
||||
// and a `target` to get it from. For convinience function is
|
||||
// implicitly curried, there for accessors can be created by invoking
|
||||
// it with just a `path` argument.
|
||||
const query = curry((path, target) => {
|
||||
const names = path.split(".");
|
||||
const count = names.length;
|
||||
let index = 0;
|
||||
let result = target;
|
||||
// Note: Permisive `!=` is intentional.
|
||||
while (result != null && index < count) {
|
||||
result = result[names[index]];
|
||||
index = index + 1;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
exports.query = query;
|
||||
|
||||
// Takes `Type` (constructor function) and a `value` and returns
|
||||
// `true` if `value` is instance of the given `Type`. Function is
|
||||
// implicitly curried this allows predicate generation by calling
|
||||
// function with just first argument.
|
||||
const isInstance = curry((Type, value) => value instanceof Type);
|
||||
exports.isInstance = isInstance;
|
||||
|
||||
/*
|
||||
* Takes a funtion and returns a wrapped function that returns `this`
|
||||
*/
|
||||
const chainable = f => derive(function(...args) {
|
||||
f.apply(this, args);
|
||||
return this;
|
||||
}, f);
|
||||
exports.chainable = chainable;
|
||||
exports.chain =
|
||||
deprecateFunction(chainable, "Function `chain` was renamed to `chainable`");
|
||||
|
||||
// Functions takes `expected` and `actual` values and returns `true` if
|
||||
// `expected === actual`. Returns curried function if called with less then
|
||||
// two arguments.
|
||||
//
|
||||
// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
|
||||
const is = curry((expected, actual) => actual === expected);
|
||||
exports.is = is;
|
||||
|
||||
const isnt = complement(is);
|
||||
exports.isnt = isnt;
|
||||
|
||||
/**
|
||||
* From underscore's `_.debounce`
|
||||
* http://underscorejs.org
|
||||
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||
* Underscore may be freely distributed under the MIT license.
|
||||
*/
|
||||
const debounce = function debounce (fn, wait) {
|
||||
let timeout, args, context, timestamp, result;
|
||||
|
||||
let later = function () {
|
||||
let last = Date.now() - timestamp;
|
||||
if (last < wait) {
|
||||
timeout = setTimeout(later, wait - last);
|
||||
} else {
|
||||
timeout = null;
|
||||
result = fn.apply(context, args);
|
||||
context = args = null;
|
||||
}
|
||||
};
|
||||
|
||||
return function (...aArgs) {
|
||||
context = this;
|
||||
args = aArgs;
|
||||
timestamp = Date.now();
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(later, wait);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
exports.debounce = debounce;
|
||||
|
||||
/**
|
||||
* From underscore's `_.throttle`
|
||||
* http://underscorejs.org
|
||||
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||
* Underscore may be freely distributed under the MIT license.
|
||||
*/
|
||||
const throttle = function throttle (func, wait, options) {
|
||||
let context, args, result;
|
||||
let timeout = null;
|
||||
let previous = 0;
|
||||
options || (options = {});
|
||||
let later = function() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
};
|
||||
return function() {
|
||||
let now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
let remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
exports.throttle = throttle;
|
||||
|
||||
exports.method = method;
|
||||
exports.invoke = invoke;
|
||||
exports.partial = partial;
|
||||
exports.curry = curry;
|
||||
exports.compose = compose;
|
||||
exports.wrap = wrap;
|
||||
exports.identity = identity;
|
||||
exports.memoize = memoize;
|
||||
exports.once = once;
|
||||
exports.cache = cache;
|
||||
exports.complement = complement;
|
||||
exports.constant = constant;
|
||||
exports.when = when;
|
||||
exports.apply = apply;
|
||||
exports.flip = flip;
|
||||
exports.field = field;
|
||||
exports.query = query;
|
||||
exports.isInstance = isInstance;
|
||||
exports.chainable = chainable;
|
||||
exports.is = is;
|
||||
exports.isnt = isnt;
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/* 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/. */
|
||||
|
||||
// Disclaimer: Some of the functions in this module implement APIs from
|
||||
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
|
||||
// those goes to him.
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { arity, name, derive, invoke } = require("./helpers");
|
||||
const { setTimeout, clearTimeout, setImmediate } = require("../../timers");
|
||||
|
||||
/**
|
||||
* Takes a function and returns a wrapped one instead, calling which will call
|
||||
* original function in the next turn of event loop. This is basically utility
|
||||
* to do `setImmediate(function() { ... })`, with a difference that returned
|
||||
* function is reused, instead of creating a new one each time. This also allows
|
||||
* to use this functions as event listeners.
|
||||
*/
|
||||
const defer = f => derive(function(...args) {
|
||||
setImmediate(invoke, f, args, this);
|
||||
}, f);
|
||||
exports.defer = defer;
|
||||
// Exporting `remit` alias as `defer` may conflict with promises.
|
||||
exports.remit = defer;
|
||||
|
||||
/**
|
||||
* Much like setTimeout, invokes function after wait milliseconds. If you pass
|
||||
* the optional arguments, they will be forwarded on to the function when it is
|
||||
* invoked.
|
||||
*/
|
||||
const delay = function delay(f, ms, ...args) {
|
||||
setTimeout(() => f.apply(this, args), ms);
|
||||
};
|
||||
exports.delay = delay;
|
||||
|
||||
/**
|
||||
* From underscore's `_.debounce`
|
||||
* http://underscorejs.org
|
||||
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||
* Underscore may be freely distributed under the MIT license.
|
||||
*/
|
||||
const debounce = function debounce (fn, wait) {
|
||||
let timeout, args, context, timestamp, result;
|
||||
|
||||
let later = function () {
|
||||
let last = Date.now() - timestamp;
|
||||
if (last < wait) {
|
||||
timeout = setTimeout(later, wait - last);
|
||||
} else {
|
||||
timeout = null;
|
||||
result = fn.apply(context, args);
|
||||
context = args = null;
|
||||
}
|
||||
};
|
||||
|
||||
return function (...aArgs) {
|
||||
context = this;
|
||||
args = aArgs;
|
||||
timestamp = Date.now();
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(later, wait);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
exports.debounce = debounce;
|
||||
|
||||
/**
|
||||
* From underscore's `_.throttle`
|
||||
* http://underscorejs.org
|
||||
* (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||
* Underscore may be freely distributed under the MIT license.
|
||||
*/
|
||||
const throttle = function throttle (func, wait, options) {
|
||||
let context, args, result;
|
||||
let timeout = null;
|
||||
let previous = 0;
|
||||
options || (options = {});
|
||||
let later = function() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
};
|
||||
return function() {
|
||||
let now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
let remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
exports.throttle = throttle;
|
|
@ -0,0 +1,290 @@
|
|||
/* 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/. */
|
||||
|
||||
// Disclaimer: Some of the functions in this module implement APIs from
|
||||
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
|
||||
// those goes to him.
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
}
|
||||
const { arity, name, derive, invoke } = require("./helpers");
|
||||
|
||||
/**
|
||||
* Takes variadic numeber of functions and returns composed one.
|
||||
* Returned function pushes `this` pseudo-variable to the head
|
||||
* of the passed arguments and invokes all the functions from
|
||||
* left to right passing same arguments to them. Composite function
|
||||
* returns return value of the right most funciton.
|
||||
*/
|
||||
const method = (...lambdas) => {
|
||||
return function method(...args) {
|
||||
args.unshift(this);
|
||||
return lambdas.reduce((_, lambda) => lambda.apply(this, args),
|
||||
void(0));
|
||||
};
|
||||
};
|
||||
exports.method = method;
|
||||
|
||||
/**
|
||||
* Invokes `callee` by passing `params` as an arguments and `self` as `this`
|
||||
* pseudo-variable. Returns value that is returned by a callee.
|
||||
* @param {Function} callee
|
||||
* Function to invoke.
|
||||
* @param {Array} params
|
||||
* Arguments to invoke function with.
|
||||
* @param {Object} self
|
||||
* Object to be passed as a `this` pseudo variable.
|
||||
*/
|
||||
exports.invoke = invoke;
|
||||
|
||||
/**
|
||||
* Takes a function and bind values to one or more arguments, returning a new
|
||||
* function of smaller arity.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* The function to partial
|
||||
*
|
||||
* @returns The new function with binded values
|
||||
*/
|
||||
const partial = (f, ...curried) => {
|
||||
if (typeof(f) !== "function")
|
||||
throw new TypeError(String(f) + " is not a function");
|
||||
|
||||
let fn = derive(function(...args) {
|
||||
return f.apply(this, curried.concat(args));
|
||||
}, f);
|
||||
fn.arity = arity(f) - curried.length;
|
||||
return fn;
|
||||
};
|
||||
exports.partial = partial;
|
||||
|
||||
/**
|
||||
* Returns function with implicit currying, which will continue currying until
|
||||
* expected number of argument is collected. Expected number of arguments is
|
||||
* determined by `fn.length`. Using this with variadic functions is stupid,
|
||||
* so don't do it.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* var sum = curry(function(a, b) {
|
||||
* return a + b
|
||||
* })
|
||||
* console.log(sum(2, 2)) // 4
|
||||
* console.log(sum(2)(4)) // 6
|
||||
*/
|
||||
const curry = new function() {
|
||||
const currier = (fn, arity, params) => {
|
||||
// Function either continues to curry arguments or executes function
|
||||
// if desired arguments have being collected.
|
||||
const curried = function(...input) {
|
||||
// Prepend all curried arguments to the given arguments.
|
||||
if (params) input.unshift.apply(input, params);
|
||||
// If expected number of arguments has being collected invoke fn,
|
||||
// othrewise return curried version Otherwise continue curried.
|
||||
return (input.length >= arity) ? fn.apply(this, input) :
|
||||
currier(fn, arity, input);
|
||||
};
|
||||
curried.arity = arity - (params ? params.length : 0);
|
||||
|
||||
return curried;
|
||||
};
|
||||
|
||||
return fn => currier(fn, arity(fn));
|
||||
};
|
||||
exports.curry = curry;
|
||||
|
||||
/**
|
||||
* Returns the composition of a list of functions, where each function consumes
|
||||
* the return value of the function that follows. In math terms, composing the
|
||||
* functions `f()`, `g()`, and `h()` produces `f(g(h()))`.
|
||||
* @example
|
||||
*
|
||||
* var greet = function(name) { return "hi: " + name; };
|
||||
* var exclaim = function(statement) { return statement + "!"; };
|
||||
* var welcome = compose(exclaim, greet);
|
||||
*
|
||||
* welcome('moe'); // => 'hi: moe!'
|
||||
*/
|
||||
function compose(...lambdas) {
|
||||
return function composed(...args) {
|
||||
let index = lambdas.length;
|
||||
while (0 <= --index)
|
||||
args = [lambdas[index].apply(this, args)];
|
||||
|
||||
return args[0];
|
||||
};
|
||||
}
|
||||
exports.compose = compose;
|
||||
|
||||
/*
|
||||
* Returns the first function passed as an argument to the second,
|
||||
* allowing you to adjust arguments, run code before and after, and
|
||||
* conditionally execute the original function.
|
||||
* @example
|
||||
*
|
||||
* var hello = function(name) { return "hello: " + name; };
|
||||
* hello = wrap(hello, function(f) {
|
||||
* return "before, " + f("moe") + ", after";
|
||||
* });
|
||||
*
|
||||
* hello(); // => 'before, hello: moe, after'
|
||||
*/
|
||||
const wrap = (f, wrapper) => derive(function wrapped(...args) {
|
||||
return wrapper.apply(this, [f].concat(args));
|
||||
}, f);
|
||||
exports.wrap = wrap;
|
||||
|
||||
/**
|
||||
* Returns the same value that is used as the argument. In math: f(x) = x
|
||||
*/
|
||||
const identity = value => value;
|
||||
exports.identity = identity;
|
||||
|
||||
/**
|
||||
* Memoizes a given function by caching the computed result. Useful for
|
||||
* speeding up slow-running computations. If passed an optional hashFunction,
|
||||
* it will be used to compute the hash key for storing the result, based on
|
||||
* the arguments to the original function. The default hashFunction just uses
|
||||
* the first argument to the memoized function as the key.
|
||||
*/
|
||||
const memoize = (f, hasher) => {
|
||||
let memo = Object.create(null);
|
||||
let cache = new WeakMap();
|
||||
hasher = hasher || identity;
|
||||
return derive(function memoizer(...args) {
|
||||
const key = hasher.apply(this, args);
|
||||
const type = typeof(key);
|
||||
if (key && (type === "object" || type === "function")) {
|
||||
if (!cache.has(key))
|
||||
cache.set(key, f.apply(this, args));
|
||||
return cache.get(key);
|
||||
}
|
||||
else {
|
||||
if (!(key in memo))
|
||||
memo[key] = f.apply(this, args);
|
||||
return memo[key];
|
||||
}
|
||||
}, f);
|
||||
};
|
||||
exports.memoize = memoize;
|
||||
|
||||
/*
|
||||
* Creates a version of the function that can only be called one time. Repeated
|
||||
* calls to the modified function will have no effect, returning the value from
|
||||
* the original call. Useful for initialization functions, instead of having to
|
||||
* set a boolean flag and then check it later.
|
||||
*/
|
||||
const once = f => {
|
||||
let ran = false, cache;
|
||||
return derive(function(...args) {
|
||||
return ran ? cache : (ran = true, cache = f.apply(this, args));
|
||||
}, f);
|
||||
};
|
||||
exports.once = once;
|
||||
// export cache as once will may be conflicting with event once a lot.
|
||||
exports.cache = once;
|
||||
|
||||
// Takes a `f` function and returns a function that takes the same
|
||||
// arguments as `f`, has the same effects, if any, and returns the
|
||||
// opposite truth value.
|
||||
const complement = f => derive(function(...args) {
|
||||
return args.length < arity(f) ? complement(partial(f, ...args)) :
|
||||
!f.apply(this, args);
|
||||
}, f);
|
||||
exports.complement = complement;
|
||||
|
||||
// Constructs function that returns `x` no matter what is it
|
||||
// invoked with.
|
||||
const constant = x => _ => x;
|
||||
exports.constant = constant;
|
||||
|
||||
// Takes `p` predicate, `consequent` function and an optional
|
||||
// `alternate` function and composes function that returns
|
||||
// application of arguments over `consequent` if application over
|
||||
// `p` is `true` otherwise returns application over `alternate`.
|
||||
// If `alternate` is not a function returns `undefined`.
|
||||
const when = (p, consequent, alternate) => {
|
||||
if (typeof(alternate) !== "function" && alternate !== void(0))
|
||||
throw TypeError("alternate must be a function");
|
||||
if (typeof(consequent) !== "function")
|
||||
throw TypeError("consequent must be a function");
|
||||
|
||||
return function(...args) {
|
||||
return p.apply(this, args) ?
|
||||
consequent.apply(this, args) :
|
||||
alternate && alternate.apply(this, args);
|
||||
};
|
||||
};
|
||||
exports.when = when;
|
||||
|
||||
// Apply function that behaves as `apply` does in lisp:
|
||||
// apply(f, x, [y, z]) => f.apply(f, [x, y, z])
|
||||
// apply(f, x) => f.apply(f, [x])
|
||||
const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop()));
|
||||
exports.apply = apply;
|
||||
|
||||
// Returns function identical to given `f` but with flipped order
|
||||
// of arguments.
|
||||
const flip = f => derive(function(...args) {
|
||||
return f.apply(this, args.reverse());
|
||||
}, f);
|
||||
exports.flip = flip;
|
||||
|
||||
// Takes field `name` and `target` and returns value of that field.
|
||||
// If `target` is `null` or `undefined` it would be returned back
|
||||
// instead of attempt to access it's field. Function is implicitly
|
||||
// curried, this allows accessor function generation by calling it
|
||||
// with only `name` argument.
|
||||
const field = curry((name, target) =>
|
||||
// Note: Permisive `==` is intentional.
|
||||
target == null ? target : target[name]);
|
||||
exports.field = field;
|
||||
|
||||
// Takes `.` delimited string representing `path` to a nested field
|
||||
// and a `target` to get it from. For convinience function is
|
||||
// implicitly curried, there for accessors can be created by invoking
|
||||
// it with just a `path` argument.
|
||||
const query = curry((path, target) => {
|
||||
const names = path.split(".");
|
||||
const count = names.length;
|
||||
let index = 0;
|
||||
let result = target;
|
||||
// Note: Permisive `!=` is intentional.
|
||||
while (result != null && index < count) {
|
||||
result = result[names[index]];
|
||||
index = index + 1;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
exports.query = query;
|
||||
|
||||
// Takes `Type` (constructor function) and a `value` and returns
|
||||
// `true` if `value` is instance of the given `Type`. Function is
|
||||
// implicitly curried this allows predicate generation by calling
|
||||
// function with just first argument.
|
||||
const isInstance = curry((Type, value) => value instanceof Type);
|
||||
exports.isInstance = isInstance;
|
||||
|
||||
/*
|
||||
* Takes a funtion and returns a wrapped function that returns `this`
|
||||
*/
|
||||
const chainable = f => derive(function(...args) {
|
||||
f.apply(this, args);
|
||||
return this;
|
||||
}, f);
|
||||
exports.chainable = chainable;
|
||||
|
||||
// Functions takes `expected` and `actual` values and returns `true` if
|
||||
// `expected === actual`. Returns curried function if called with less then
|
||||
// two arguments.
|
||||
//
|
||||
// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
|
||||
const is = curry((expected, actual) => actual === expected);
|
||||
exports.is = is;
|
||||
|
||||
const isnt = complement(is);
|
||||
exports.isnt = isnt;
|
|
@ -0,0 +1,29 @@
|
|||
/* 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/. */
|
||||
|
||||
// Disclaimer: Some of the functions in this module implement APIs from
|
||||
// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
|
||||
// those goes to him.
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
}
|
||||
|
||||
const arity = f => f.arity || f.length;
|
||||
exports.arity = arity;
|
||||
|
||||
const name = f => f.displayName || f.name;
|
||||
exports.name = name;
|
||||
|
||||
const derive = (f, source) => {
|
||||
f.displayName = name(source);
|
||||
f.arity = arity(source);
|
||||
return f;
|
||||
};
|
||||
exports.derive = derive;
|
||||
|
||||
const invoke = (callee, params, self) => callee.apply(self, params);
|
||||
exports.invoke = invoke;
|
|
@ -19,7 +19,7 @@ const { EventTarget } = require('./event/target');
|
|||
const { on, emit, once, setListeners } = require('./event/core');
|
||||
const { on: domOn, removeListener: domOff } = require('./dom/events');
|
||||
const { pipe } = require('./event/utils');
|
||||
const { isRegExp } = require('./lang/type');
|
||||
const { isRegExp, isUndefined } = require('./lang/type');
|
||||
const { merge } = require('./util/object');
|
||||
const { windowIterator } = require('./deprecated/window-utils');
|
||||
const { isBrowser, getFrames } = require('./window/utils');
|
||||
|
@ -48,7 +48,9 @@ let styleFor = (mod) => styles.get(mod);
|
|||
observers.on('document-element-inserted', onContentWindow);
|
||||
unload(() => observers.off('document-element-inserted', onContentWindow));
|
||||
|
||||
// Helper functions
|
||||
let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
|
||||
let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
|
||||
|
||||
// Validation Contracts
|
||||
const modOptions = {
|
||||
|
@ -71,6 +73,19 @@ const modOptions = {
|
|||
},
|
||||
msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.'
|
||||
},
|
||||
exclude: {
|
||||
is: ['string', 'array', 'regexp', 'undefined'],
|
||||
ok: (rule) => {
|
||||
if (isRegExpOrString(rule) || isUndefined(rule))
|
||||
return true;
|
||||
if (Array.isArray(rule) && rule.length > 0)
|
||||
return rule.every(isRegExpOrString);
|
||||
return false;
|
||||
},
|
||||
msg: 'If set, the `exclude` option must always contain at least one ' +
|
||||
'rule as a string, regular expression, or an array of strings and ' +
|
||||
'regular expressions.'
|
||||
},
|
||||
attachTo: {
|
||||
is: ['string', 'array', 'undefined'],
|
||||
map: function (attachTo) {
|
||||
|
@ -115,6 +130,10 @@ const PageMod = Class({
|
|||
model.include = Rules();
|
||||
model.include.add.apply(model.include, [].concat(include));
|
||||
|
||||
let exclude = isUndefined(model.exclude) ? [] : model.exclude;
|
||||
model.exclude = Rules();
|
||||
model.exclude.add.apply(model.exclude, [].concat(exclude));
|
||||
|
||||
if (model.contentStyle || model.contentStyleFile) {
|
||||
styles.set(mod, Style({
|
||||
uri: model.contentStyleFile,
|
||||
|
@ -162,7 +181,7 @@ function onContentWindow({ subject: document }) {
|
|||
return;
|
||||
|
||||
for (let pagemod of pagemods) {
|
||||
if (pagemod.include.matchesAny(document.URL))
|
||||
if (modMatchesURI(pagemod, document.URL))
|
||||
onContent(pagemod, window);
|
||||
}
|
||||
}
|
||||
|
@ -171,13 +190,13 @@ function applyOnExistingDocuments (mod) {
|
|||
getTabs().forEach(tab => {
|
||||
// Fake a newly created document
|
||||
let window = getTabContentWindow(tab);
|
||||
if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab)))
|
||||
let uri = getTabURI(tab);
|
||||
if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
|
||||
onContent(mod, window);
|
||||
if (has(mod.attachTo, "frame")) {
|
||||
if (has(mod.attachTo, "frame"))
|
||||
getFrames(window).
|
||||
filter((iframe) => mod.include.matchesAny(iframe.location.href)).
|
||||
forEach((frame) => onContent(mod, frame));
|
||||
}
|
||||
filter(iframe => modMatchesURI(mod, iframe.location.href)).
|
||||
forEach(frame => onContent(mod, frame));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci, Cu } = require('chrome');
|
||||
const { on } = require('../system/events');
|
||||
const { id, preferencesBranch } = require('../self');
|
||||
const { localizeInlineOptions } = require('../l10n/prefs');
|
||||
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
|
||||
const DEFAULT_OPTIONS_URL = 'data:text/xml,<placeholder/>';
|
||||
|
||||
const VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color',
|
||||
'file', 'directory', 'control', 'menulist', 'radio'];
|
||||
|
||||
function enable(preferences) {
|
||||
validate(preferences);
|
||||
setDefaults(preferences, preferencesBranch);
|
||||
|
||||
// allow the use of custom options.xul
|
||||
AddonManager.getAddonByID(id, (addon) => {
|
||||
if (addon.optionsURL === DEFAULT_OPTIONS_URL)
|
||||
on('addon-options-displayed', onAddonOptionsDisplayed, true);
|
||||
})
|
||||
|
||||
function onAddonOptionsDisplayed({ subject: doc, data }) {
|
||||
if (data === id) {
|
||||
let parent = doc.getElementById('detail-downloads').parentNode;
|
||||
injectOptions(preferences, preferencesBranch, doc, parent);
|
||||
localizeInlineOptions(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.enable = enable;
|
||||
|
||||
// centralized sanity checks
|
||||
function validate(preferences) {
|
||||
for (let { name, title, type, label, options } of preferences) {
|
||||
// make sure the title is set and non-empty
|
||||
if (!title)
|
||||
throw Error("The '" + name + "' pref requires a title");
|
||||
|
||||
// make sure that pref type is a valid inline option type
|
||||
if (!~VALID_PREF_TYPES.indexOf(type))
|
||||
throw Error("The '" + name + "' pref must be of valid type");
|
||||
|
||||
// if it's a control, make sure it has a label
|
||||
if (type === 'control' && !label)
|
||||
throw Error("The '" + name + "' control requires a label");
|
||||
|
||||
// if it's a menulist or radio, make sure it has options
|
||||
if (type === 'menulist' || type === 'radio') {
|
||||
if (!options)
|
||||
throw Error("The '" + name + "' pref requires options");
|
||||
|
||||
// make sure each option has a value and a label
|
||||
for (let item of options) {
|
||||
if (!('value' in item) || !('label' in item))
|
||||
throw Error("Each option requires both a value and a label");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check that pref type matches default value type
|
||||
}
|
||||
}
|
||||
exports.validate = validate;
|
||||
|
||||
// initializes default preferences, emulates defaults/prefs.js
|
||||
function setDefaults(preferences, preferencesBranch) {
|
||||
const branch = Cc['@mozilla.org/preferences-service;1'].
|
||||
getService(Ci.nsIPrefService).
|
||||
getDefaultBranch('extensions.' + preferencesBranch + '.');
|
||||
for (let {name, value} of preferences) {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
branch.setBoolPref(name, value);
|
||||
break;
|
||||
case 'number':
|
||||
// must be integer, ignore otherwise
|
||||
if (value % 1 === 0)
|
||||
branch.setIntPref(name, value);
|
||||
break;
|
||||
case 'string':
|
||||
// ∵
|
||||
let str = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
str.data = value;
|
||||
branch.setComplexValue(name, Ci.nsISupportsString, str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.setDefaults = setDefaults;
|
||||
|
||||
// dynamically injects inline options into about:addons page at runtime
|
||||
function injectOptions(preferences, preferencesBranch, document, parent) {
|
||||
for (let { name, type, hidden, title, description, label, options, on, off } of preferences) {
|
||||
|
||||
if (hidden)
|
||||
continue;
|
||||
|
||||
let setting = document.createElement('setting');
|
||||
setting.setAttribute('pref-name', name);
|
||||
setting.setAttribute('data-jetpack-id', id);
|
||||
setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name);
|
||||
setting.setAttribute('type', type);
|
||||
setting.setAttribute('title', title);
|
||||
setting.setAttribute('desc', description);
|
||||
|
||||
if (type === 'file' || type === 'directory') {
|
||||
setting.setAttribute('fullpath', 'true');
|
||||
}
|
||||
else if (type === 'control') {
|
||||
let button = document.createElement('button');
|
||||
button.setAttribute('pref-name', name);
|
||||
button.setAttribute('data-jetpack-id', id);
|
||||
button.setAttribute('label', label);
|
||||
button.setAttribute('oncommand', "Services.obs.notifyObservers(null, '" +
|
||||
id + "-cmdPressed', '" + name + "');");
|
||||
setting.appendChild(button);
|
||||
}
|
||||
else if (type === 'boolint') {
|
||||
setting.setAttribute('on', on);
|
||||
setting.setAttribute('off', off);
|
||||
}
|
||||
else if (type === 'menulist') {
|
||||
let menulist = document.createElement('menulist');
|
||||
let menupopup = document.createElement('menupopup');
|
||||
for (let { value, label } of options) {
|
||||
let menuitem = document.createElement('menuitem');
|
||||
menuitem.setAttribute('value', value);
|
||||
menuitem.setAttribute('label', label);
|
||||
menupopup.appendChild(menuitem);
|
||||
}
|
||||
menulist.appendChild(menupopup);
|
||||
setting.appendChild(menulist);
|
||||
}
|
||||
else if (type === 'radio') {
|
||||
let radiogroup = document.createElement('radiogroup');
|
||||
for (let { value, label } of options) {
|
||||
let radio = document.createElement('radio');
|
||||
radio.setAttribute('value', value);
|
||||
radio.setAttribute('label', label);
|
||||
radiogroup.appendChild(radio);
|
||||
}
|
||||
setting.appendChild(radiogroup);
|
||||
}
|
||||
|
||||
parent.appendChild(setting);
|
||||
}
|
||||
}
|
||||
exports.injectOptions = injectOptions;
|
|
@ -815,9 +815,6 @@ def run(arguments=sys.argv[1:], target_cfg=None, pkg_cfg=None,
|
|||
mydir = os.path.dirname(os.path.abspath(__file__))
|
||||
app_extension_dir = os.path.join(mydir, "../../app-extension")
|
||||
|
||||
if target_cfg.get('preferences'):
|
||||
harness_options['preferences'] = target_cfg.get('preferences')
|
||||
|
||||
# Do not add entries for SDK modules
|
||||
harness_options['manifest'] = manifest.get_harness_options_manifest(False)
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# 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/.
|
||||
|
||||
def parse_options_defaults(options, preferencesBranch):
|
||||
# this returns a unicode string
|
||||
pref_list = []
|
||||
|
||||
for pref in options:
|
||||
if ('value' in pref):
|
||||
value = pref["value"]
|
||||
|
||||
if isinstance(value, float):
|
||||
continue
|
||||
elif isinstance(value, bool):
|
||||
value = str(pref["value"]).lower()
|
||||
elif isinstance(value, str): # presumably ASCII
|
||||
value = "\"" + unicode(pref["value"]) + "\""
|
||||
elif isinstance(value, unicode):
|
||||
value = "\"" + pref["value"] + "\""
|
||||
else:
|
||||
value = str(pref["value"])
|
||||
|
||||
pref_list.append("pref(\"extensions." + preferencesBranch + "." + pref["name"] + "\", " + value + ");")
|
||||
|
||||
return "\n".join(pref_list) + "\n"
|
|
@ -1,101 +0,0 @@
|
|||
# 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/.
|
||||
|
||||
from xml.dom.minidom import Document
|
||||
|
||||
VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color', 'file',
|
||||
'directory', 'control', 'menulist', 'radio']
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
class BadPrefTypeError(Error):
|
||||
pass
|
||||
|
||||
class MissingPrefAttr(Error):
|
||||
pass
|
||||
|
||||
def validate_prefs(options):
|
||||
for pref in options:
|
||||
# Make sure there is a 'title'
|
||||
if ("title" not in pref):
|
||||
raise MissingPrefAttr("The '%s' pref requires a 'title'" % (pref["name"]))
|
||||
|
||||
# Make sure that the pref type is a valid inline pref type
|
||||
if (pref["type"] not in VALID_PREF_TYPES):
|
||||
raise BadPrefTypeError('%s is not a valid inline pref type' % (pref["type"]))
|
||||
|
||||
# Make sure the 'control' type has a 'label'
|
||||
if (pref["type"] == "control"):
|
||||
if ("label" not in pref):
|
||||
raise MissingPrefAttr("The 'control' inline pref type requires a 'label'")
|
||||
|
||||
# Make sure the 'menulist' type has a 'menulist'
|
||||
if (pref["type"] == "menulist" or pref["type"] == "radio"):
|
||||
if ("options" not in pref):
|
||||
raise MissingPrefAttr("The 'menulist' and the 'radio' inline pref types requires a 'options'")
|
||||
|
||||
# Make sure each option has a 'value' and a 'label'
|
||||
for item in pref["options"]:
|
||||
if ("value" not in item):
|
||||
raise MissingPrefAttr("'options' requires a 'value'")
|
||||
if ("label" not in item):
|
||||
raise MissingPrefAttr("'options' requires a 'label'")
|
||||
|
||||
# TODO: Check that pref["type"] matches default value type
|
||||
|
||||
def parse_options(options, jetpack_id, preferencesBranch):
|
||||
doc = Document()
|
||||
root = doc.createElement("vbox")
|
||||
root.setAttribute("xmlns", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul")
|
||||
doc.appendChild(root)
|
||||
|
||||
for pref in options:
|
||||
if ("hidden" in pref and pref["hidden"] == True):
|
||||
continue;
|
||||
|
||||
setting = doc.createElement("setting")
|
||||
setting.setAttribute("pref-name", pref["name"])
|
||||
setting.setAttribute("data-jetpack-id", jetpack_id)
|
||||
setting.setAttribute("pref", "extensions." + preferencesBranch + "." + pref["name"])
|
||||
setting.setAttribute("type", pref["type"])
|
||||
setting.setAttribute("title", pref["title"])
|
||||
|
||||
if ("description" in pref):
|
||||
setting.appendChild(doc.createTextNode(pref["description"]))
|
||||
|
||||
if (pref["type"] == "control"):
|
||||
button = doc.createElement("button")
|
||||
button.setAttribute("pref-name", pref["name"])
|
||||
button.setAttribute("data-jetpack-id", jetpack_id)
|
||||
button.setAttribute("label", pref["label"])
|
||||
button.setAttribute("oncommand", "Services.obs.notifyObservers(null, '" +
|
||||
jetpack_id + "-cmdPressed', '" +
|
||||
pref["name"] + "');");
|
||||
setting.appendChild(button)
|
||||
elif (pref["type"] == "boolint"):
|
||||
setting.setAttribute("on", pref["on"])
|
||||
setting.setAttribute("off", pref["off"])
|
||||
elif (pref["type"] == "menulist"):
|
||||
menulist = doc.createElement("menulist")
|
||||
menupopup = doc.createElement("menupopup")
|
||||
for item in pref["options"]:
|
||||
menuitem = doc.createElement("menuitem")
|
||||
menuitem.setAttribute("value", item["value"])
|
||||
menuitem.setAttribute("label", item["label"])
|
||||
menupopup.appendChild(menuitem)
|
||||
menulist.appendChild(menupopup)
|
||||
setting.appendChild(menulist)
|
||||
elif (pref["type"] == "radio"):
|
||||
radiogroup = doc.createElement("radiogroup")
|
||||
for item in pref["options"]:
|
||||
radio = doc.createElement("radio")
|
||||
radio.setAttribute("value", item["value"])
|
||||
radio.setAttribute("label", item["label"])
|
||||
radiogroup.appendChild(radio)
|
||||
setting.appendChild(radiogroup)
|
||||
|
||||
root.appendChild(setting)
|
||||
|
||||
return doc.toprettyxml(indent=" ")
|
|
@ -23,7 +23,7 @@ DEFAULT_ICON64 = 'icon64.png'
|
|||
|
||||
METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version',
|
||||
'translators', 'contributors', 'license', 'homepage', 'icon',
|
||||
'icon64', 'main', 'directories', 'permissions']
|
||||
'icon64', 'main', 'directories', 'permissions', 'preferences']
|
||||
|
||||
RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$')
|
||||
|
||||
|
@ -393,9 +393,6 @@ def generate_build_for_target(pkg_cfg, target, deps,
|
|||
build['icon64'] = os.path.join(target_cfg.root_dir, target_cfg.icon64)
|
||||
del target_cfg['icon64']
|
||||
|
||||
if ('preferences' in target_cfg):
|
||||
build['preferences'] = target_cfg.preferences
|
||||
|
||||
if 'id' in target_cfg:
|
||||
# NOTE: logic duplicated from buildJID()
|
||||
jid = target_cfg['id']
|
||||
|
|
|
@ -150,8 +150,17 @@ def gen_manifest(template_root_dir, target_cfg, jid,
|
|||
|
||||
if target_cfg.get("preferences"):
|
||||
manifest.set("em:optionsType", "2")
|
||||
|
||||
# workaround until bug 971249 is fixed
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=971249
|
||||
manifest.set("em:optionsURL", "data:text/xml,<placeholder/>")
|
||||
|
||||
# workaround for workaround, for testing simple-prefs-regression
|
||||
if (os.path.exists(os.path.join(template_root_dir, "options.xul"))):
|
||||
manifest.remove("em:optionsURL")
|
||||
else:
|
||||
manifest.remove("em:optionsType")
|
||||
manifest.remove("em:optionsURL")
|
||||
|
||||
if enable_mobile:
|
||||
target_app = dom.createElement("em:targetApplication")
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
/* 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/. */
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/* 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/. */
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/* 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/. */
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/* 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/. */
|
||||
|
|
@ -20,128 +20,6 @@ xpi_template_path = os.path.join(test_packaging.static_files_path,
|
|||
|
||||
fake_manifest = '<RDF><!-- Extension metadata is here. --></RDF>'
|
||||
|
||||
class PrefsTests(unittest.TestCase):
|
||||
def makexpi(self, pkg_name):
|
||||
self.xpiname = "%s.xpi" % pkg_name
|
||||
create_xpi(self.xpiname, pkg_name, 'preferences-files')
|
||||
self.xpi = zipfile.ZipFile(self.xpiname, 'r')
|
||||
options = self.xpi.read('harness-options.json')
|
||||
self.xpi_harness_options = json.loads(options)
|
||||
|
||||
def setUp(self):
|
||||
self.xpiname = None
|
||||
self.xpi = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.xpi:
|
||||
self.xpi.close()
|
||||
if self.xpiname and os.path.exists(self.xpiname):
|
||||
os.remove(self.xpiname)
|
||||
|
||||
def testPackageWithSimplePrefs(self):
|
||||
self.makexpi('simple-prefs')
|
||||
packageName = 'jid1-fZHqN9JfrDBa8A@jetpack'
|
||||
self.failUnless('options.xul' in self.xpi.namelist())
|
||||
optsxul = self.xpi.read('options.xul').decode("utf-8")
|
||||
self.failUnlessEqual(self.xpi_harness_options["jetpackID"], packageName)
|
||||
self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], packageName)
|
||||
|
||||
root = ElementTree.XML(optsxul.encode('utf-8'))
|
||||
|
||||
xulNamespacePrefix = \
|
||||
"{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
|
||||
|
||||
settings = root.findall(xulNamespacePrefix + 'setting')
|
||||
|
||||
def assertPref(setting, name, prefType, title):
|
||||
self.failUnlessEqual(setting.get('data-jetpack-id'), packageName)
|
||||
self.failUnlessEqual(setting.get('pref'),
|
||||
'extensions.' + packageName + '.' + name)
|
||||
self.failUnlessEqual(setting.get('pref-name'), name)
|
||||
self.failUnlessEqual(setting.get('type'), prefType)
|
||||
self.failUnlessEqual(setting.get('title'), title)
|
||||
|
||||
assertPref(settings[0], 'test', 'bool', u't\u00EBst')
|
||||
assertPref(settings[1], 'test2', 'string', u't\u00EBst')
|
||||
assertPref(settings[2], 'test3', 'menulist', '"><test')
|
||||
assertPref(settings[3], 'test4', 'radio', u't\u00EBst')
|
||||
|
||||
menuItems = settings[2].findall(
|
||||
'%(0)smenulist/%(0)smenupopup/%(0)smenuitem' % { "0": xulNamespacePrefix })
|
||||
radios = settings[3].findall(
|
||||
'%(0)sradiogroup/%(0)sradio' % { "0": xulNamespacePrefix })
|
||||
|
||||
def assertOption(option, value, label):
|
||||
self.failUnlessEqual(option.get('value'), value)
|
||||
self.failUnlessEqual(option.get('label'), label)
|
||||
|
||||
assertOption(menuItems[0], "0", "label1")
|
||||
assertOption(menuItems[1], "1", "label2")
|
||||
assertOption(radios[0], "red", "rouge")
|
||||
assertOption(radios[1], "blue", "bleu")
|
||||
|
||||
prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
|
||||
exp = [u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test", false);',
|
||||
u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test2", "\u00FCnic\u00F8d\u00E9");',
|
||||
u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test3", "1");',
|
||||
u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test4", "red");',
|
||||
]
|
||||
self.failUnlessEqual(prefsjs, "\n".join(exp)+"\n")
|
||||
|
||||
def testPackageWithPreferencesBranch(self):
|
||||
self.makexpi('preferences-branch')
|
||||
self.failUnless('options.xul' in self.xpi.namelist())
|
||||
optsxul = self.xpi.read('options.xul').decode("utf-8")
|
||||
self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"],
|
||||
"human-readable")
|
||||
|
||||
root = ElementTree.XML(optsxul.encode('utf-8'))
|
||||
xulNamespacePrefix = \
|
||||
"{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
|
||||
|
||||
setting = root.find(xulNamespacePrefix + 'setting')
|
||||
self.failUnlessEqual(setting.get('pref'),
|
||||
'extensions.human-readable.test42')
|
||||
|
||||
prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
|
||||
self.failUnlessEqual(prefsjs,
|
||||
'pref("extensions.human-readable.test42", true);\n')
|
||||
|
||||
def testPackageWithNoPrefs(self):
|
||||
self.makexpi('no-prefs')
|
||||
self.failIf('options.xul' in self.xpi.namelist())
|
||||
self.failUnlessEqual(self.xpi_harness_options["jetpackID"],
|
||||
"jid1-fZHqN9JfrDBa8A@jetpack")
|
||||
prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
|
||||
self.failUnlessEqual(prefsjs, "")
|
||||
|
||||
def testPackageWithInvalidPreferencesBranch(self):
|
||||
self.makexpi('curly-id')
|
||||
self.failIfEqual(self.xpi_harness_options["preferencesBranch"],
|
||||
"invalid^branch*name")
|
||||
self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"],
|
||||
"{34a1eae1-c20a-464f-9b0e-000000000000}")
|
||||
|
||||
def testPackageWithCurlyID(self):
|
||||
self.makexpi('curly-id')
|
||||
self.failUnlessEqual(self.xpi_harness_options["jetpackID"],
|
||||
"{34a1eae1-c20a-464f-9b0e-000000000000}")
|
||||
|
||||
self.failUnless('options.xul' in self.xpi.namelist())
|
||||
optsxul = self.xpi.read('options.xul').decode("utf-8")
|
||||
|
||||
root = ElementTree.XML(optsxul.encode('utf-8'))
|
||||
xulNamespacePrefix = \
|
||||
"{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
|
||||
|
||||
setting = root.find(xulNamespacePrefix + 'setting')
|
||||
self.failUnlessEqual(setting.get('pref'),
|
||||
'extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13')
|
||||
|
||||
prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
|
||||
self.failUnlessEqual(prefsjs,
|
||||
'pref("extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13", 26);\n')
|
||||
|
||||
|
||||
class Bug588119Tests(unittest.TestCase):
|
||||
def makexpi(self, pkg_name):
|
||||
|
@ -300,7 +178,6 @@ class SmallXPI(unittest.TestCase):
|
|||
# one in tests/static-files/xpi-template doesn't
|
||||
"harness-options.json",
|
||||
"install.rdf",
|
||||
"defaults/preferences/prefs.js",
|
||||
"resources/",
|
||||
"resources/addon-sdk/",
|
||||
"resources/addon-sdk/lib/",
|
||||
|
|
|
@ -70,31 +70,6 @@ def build_xpi(template_root_dir, manifest, xpi_path,
|
|||
])
|
||||
files_to_copy[str(arcpath)] = str(abspath)
|
||||
|
||||
# Handle simple-prefs
|
||||
if 'preferences' in harness_options:
|
||||
from options_xul import parse_options, validate_prefs
|
||||
|
||||
validate_prefs(harness_options["preferences"])
|
||||
|
||||
opts_xul = parse_options(harness_options["preferences"],
|
||||
harness_options["jetpackID"],
|
||||
harness_options["preferencesBranch"])
|
||||
open('.options.xul', 'wb').write(opts_xul.encode("utf-8"))
|
||||
zf.write('.options.xul', 'options.xul')
|
||||
os.remove('.options.xul')
|
||||
|
||||
from options_defaults import parse_options_defaults
|
||||
prefs_js = parse_options_defaults(harness_options["preferences"],
|
||||
harness_options["preferencesBranch"])
|
||||
open('.prefs.js', 'wb').write(prefs_js.encode("utf-8"))
|
||||
|
||||
else:
|
||||
open('.prefs.js', 'wb').write("")
|
||||
|
||||
zf.write('.prefs.js', 'defaults/preferences/prefs.js')
|
||||
os.remove('.prefs.js')
|
||||
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(template_root_dir):
|
||||
filenames = list(filter_filenames(filenames, IGNORED_FILES))
|
||||
dirnames[:] = filter_dirnames(dirnames)
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<em:creator>Mozilla Corporation</em:creator>
|
||||
<em:homepageURL></em:homepageURL>
|
||||
<em:optionsType></em:optionsType>
|
||||
<em:optionsURL></em:optionsURL>
|
||||
<em:updateURL></em:updateURL>
|
||||
</Description>
|
||||
</RDF>
|
||||
|
|
|
@ -73,6 +73,7 @@ if (app.is('Firefox')) {
|
|||
'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' +
|
||||
'setTimeout(function() {\n' + // TODO: figure out why this is necessary..
|
||||
'self.postMessage({\n' +
|
||||
'someCount: unsafeWindow.document.querySelectorAll("setting[title=\'some-title\']").length,\n' +
|
||||
'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[title=\'some-title\']")),\n' +
|
||||
'myInteger: getAttributes(unsafeWindow.document.querySelector("setting[title=\'my-int\']")),\n' +
|
||||
'myHiddenInt: getAttributes(unsafeWindow.document.querySelector("setting[title=\'hidden-int\']")),\n' +
|
||||
|
@ -100,6 +101,9 @@ if (app.is('Firefox')) {
|
|||
'unsafeWindow.addEventListener("load", onLoad, false);\n' +
|
||||
'}\n',
|
||||
onMessage: function(msg) {
|
||||
// test against doc caching
|
||||
assert.equal(msg.someCount, 1, 'there is exactly one <setting> node for somePreference');
|
||||
|
||||
// test somePreference
|
||||
assert.equal(msg.somePreference.type, 'string', 'some pref is a string');
|
||||
assert.equal(msg.somePreference.pref, 'extensions.'+self.id+'.somePreference', 'somePreference path is correct');
|
||||
|
@ -129,6 +133,11 @@ if (app.is('Firefox')) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run it again, to test against inline options document caching
|
||||
// and duplication of <setting> nodes upon re-entry to about:addons
|
||||
exports.testAgainstDocCaching = exports.testAOM;
|
||||
|
||||
}
|
||||
|
||||
exports.testDefaultPreferencesBranch = function(assert) {
|
||||
|
|
|
@ -45,6 +45,30 @@
|
|||
"label": "bleu"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test5",
|
||||
"type": "boolint",
|
||||
"title": "part part, particle",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"name": "test6",
|
||||
"type": "color",
|
||||
"title": "pop pop, popscicle",
|
||||
"value": "#ff5e99"
|
||||
},
|
||||
{
|
||||
"name": "test7",
|
||||
"type": "file",
|
||||
"title": "bike bike",
|
||||
"value": "bicycle"
|
||||
},
|
||||
{
|
||||
"name": "test8",
|
||||
"type": "directory",
|
||||
"title": "test test",
|
||||
"value": "1-2-3"
|
||||
}],
|
||||
"loader": "lib/main.js"
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/* 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";
|
||||
|
||||
const { Request } = require("sdk/request");
|
||||
|
||||
exports.testBootstrapExists = function (assert, done) {
|
||||
Request({
|
||||
url: "resource://gre/modules/sdk/bootstrap.js",
|
||||
onComplete: function (response) {
|
||||
if (response.text)
|
||||
assert.pass("the bootstrap file was found");
|
||||
done();
|
||||
}
|
||||
}).get();
|
||||
};
|
||||
|
||||
require("sdk/test").run(exports);
|
|
@ -0,0 +1,144 @@
|
|||
/* 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";
|
||||
|
||||
const { setDefaults, injectOptions, validate } = require('sdk/preferences/native-options');
|
||||
const { activeBrowserWindow: { document } } = require("sdk/deprecated/window-utils");
|
||||
const { preferencesBranch, id } = require('sdk/self');
|
||||
const { get } = require('sdk/preferences/service');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const simple = require('sdk/simple-prefs');
|
||||
const fixtures = require('./fixtures');
|
||||
const { Cc, Ci } = require('chrome');
|
||||
|
||||
exports.testValidate = function(assert) {
|
||||
let { preferences } = packageJSON('simple-prefs');
|
||||
|
||||
let block = () => validate(preferences);
|
||||
|
||||
delete preferences[3].options[0].value;
|
||||
assert.throws(block, /option requires both a value/, "option missing value error");
|
||||
|
||||
delete preferences[2].options;
|
||||
assert.throws(block, /'test3' pref requires options/, "menulist missing options error");
|
||||
|
||||
preferences[1].type = 'control';
|
||||
assert.throws(block, /'test2' control requires a label/, "control missing label error");
|
||||
|
||||
preferences[1].type = 'nonvalid';
|
||||
assert.throws(block, /'test2' pref must be of valid type/, "invalid pref type error");
|
||||
|
||||
delete preferences[0].title;
|
||||
assert.throws(block, /'test' pref requires a title/, "pref missing title error");
|
||||
}
|
||||
|
||||
exports.testNoPrefs = function(assert, done) {
|
||||
let { preferences } = packageJSON('no-prefs');
|
||||
|
||||
let parent = document.createDocumentFragment();
|
||||
injectOptions(preferences || [], preferencesBranch, document, parent);
|
||||
assert.equal(parent.children.length, 0, "No setting elements injected");
|
||||
|
||||
// must test with events because we can't reset default prefs
|
||||
function onPrefChange(name) {
|
||||
assert.fail("No preferences should be defined");
|
||||
}
|
||||
|
||||
simple.on('', onPrefChange);
|
||||
setDefaults(preferences || [], preferencesBranch);
|
||||
setTimeout(function() {
|
||||
assert.pass("No preferences were defined");
|
||||
simple.off('', onPrefChange);
|
||||
done();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
exports.testCurlyID = function(assert) {
|
||||
let { preferences, id } = packageJSON('curly-id');
|
||||
|
||||
let parent = document.createDocumentFragment();
|
||||
injectOptions(preferences, id, document, parent);
|
||||
assert.equal(parent.children.length, 1, "One setting elements injected");
|
||||
assert.equal(parent.firstElementChild.attributes.pref.value,
|
||||
"extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13",
|
||||
"Setting pref attribute is set properly");
|
||||
|
||||
setDefaults(preferences, id);
|
||||
assert.equal(get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13'),
|
||||
26, "test13 is 26");
|
||||
}
|
||||
|
||||
exports.testPreferencesBranch = function(assert) {
|
||||
let { preferences, 'preferences-branch': prefsBranch } = packageJSON('preferences-branch');
|
||||
|
||||
let parent = document.createDocumentFragment();
|
||||
injectOptions(preferences, prefsBranch, document, parent);
|
||||
assert.equal(parent.children.length, 1, "One setting elements injected");
|
||||
assert.equal(parent.firstElementChild.attributes.pref.value,
|
||||
"extensions.human-readable.test42",
|
||||
"Setting pref attribute is set properly");
|
||||
|
||||
setDefaults(preferences, prefsBranch);
|
||||
assert.equal(get('extensions.human-readable.test42'), true, "test42 is true");
|
||||
}
|
||||
|
||||
exports.testSimplePrefs = function(assert) {
|
||||
let { preferences } = packageJSON('simple-prefs');
|
||||
|
||||
function assertPref(setting, name, type, title) {
|
||||
assert.equal(setting.getAttribute('data-jetpack-id'), id,
|
||||
"setting 'data-jetpack-id' attribute correct");
|
||||
assert.equal(setting.getAttribute('pref'), 'extensions.' + id + '.' + name,
|
||||
"setting 'pref' attribute correct");
|
||||
assert.equal(setting.getAttribute('pref-name'), name,
|
||||
"setting 'pref-name' attribute correct");
|
||||
assert.equal(setting.getAttribute('type'), type,
|
||||
"setting 'type' attribute correct");
|
||||
assert.equal(setting.getAttribute('title'), title,
|
||||
"setting 'title' attribute correct");
|
||||
}
|
||||
|
||||
function assertOption(option, value, label) {
|
||||
assert.equal(option.getAttribute('value'), value, "value attribute correct");
|
||||
assert.equal(option.getAttribute('label'), label, "label attribute correct");
|
||||
}
|
||||
|
||||
let parent = document.createDocumentFragment();
|
||||
injectOptions(preferences, preferencesBranch, document, parent);
|
||||
assert.equal(parent.children.length, 8, "Eight setting elements injected");
|
||||
|
||||
assertPref(parent.children[0], 'test', 'bool', 't\u00EBst');
|
||||
assertPref(parent.children[1], 'test2', 'string', 't\u00EBst');
|
||||
assertPref(parent.children[2], 'test3', 'menulist', '"><test');
|
||||
assertPref(parent.children[3], 'test4', 'radio', 't\u00EBst');
|
||||
|
||||
assertPref(parent.children[4], 'test5', 'boolint', 'part part, particle');
|
||||
assertPref(parent.children[5], 'test6', 'color', 'pop pop, popscicle');
|
||||
assertPref(parent.children[6], 'test7', 'file', 'bike bike');
|
||||
assertPref(parent.children[7], 'test8', 'directory', 'test test');
|
||||
|
||||
let menuItems = parent.children[2].querySelectorAll('menupopup>menuitem');
|
||||
let radios = parent.children[3].querySelectorAll('radiogroup>radio');
|
||||
|
||||
assertOption(menuItems[0], '0', 'label1');
|
||||
assertOption(menuItems[1], '1', 'label2');
|
||||
assertOption(radios[0], 'red', 'rouge');
|
||||
assertOption(radios[1], 'blue', 'bleu');
|
||||
|
||||
setDefaults(preferences, preferencesBranch);
|
||||
assert.strictEqual(simple.prefs.test, false, "test is false");
|
||||
assert.strictEqual(simple.prefs.test2, "\u00FCnic\u00F8d\u00E9", "test2 is unicode");
|
||||
assert.strictEqual(simple.prefs.test3, "1", "test3 is '1'");
|
||||
assert.strictEqual(simple.prefs.test4, "red", "test4 is 'red'");
|
||||
|
||||
// default pref branch can't be "reset", bug 1012231
|
||||
Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService).
|
||||
getDefaultBranch('extensions.' + preferencesBranch).deleteBranch('');
|
||||
}
|
||||
|
||||
function packageJSON(dir) {
|
||||
return require(fixtures.url('preferences/' + dir + '/package.json'));
|
||||
}
|
||||
|
||||
require('sdk/test').run(exports);
|
|
@ -132,16 +132,61 @@ exports.testPageModIncludes = function(assert, done) {
|
|||
createPageModTest(testPageURI, true)
|
||||
],
|
||||
function (win, done) {
|
||||
waitUntil(function () win.localStorage[testPageURI],
|
||||
testPageURI + " page-mod to be executed")
|
||||
.then(function () {
|
||||
asserts.forEach(function(fn) {
|
||||
fn(assert, win);
|
||||
});
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
waitUntil(() => win.localStorage[testPageURI],
|
||||
testPageURI + " page-mod to be executed")
|
||||
.then(() => {
|
||||
asserts.forEach(fn => fn(assert, win));
|
||||
win.localStorage.clear();
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testPageModExcludes = function(assert, done) {
|
||||
var asserts = [];
|
||||
function createPageModTest(include, exclude, expectedMatch) {
|
||||
// Create an 'onload' test function...
|
||||
asserts.push(function(test, win) {
|
||||
var matches = JSON.stringify([include, exclude]) in win.localStorage;
|
||||
assert.ok(expectedMatch ? matches : !matches,
|
||||
"[include, exclude] = [" + include + ", " + exclude +
|
||||
"] match test, expected: " + expectedMatch);
|
||||
});
|
||||
// ...and corresponding PageMod options
|
||||
return {
|
||||
include: include,
|
||||
exclude: exclude,
|
||||
contentScript: 'new ' + function() {
|
||||
self.on("message", function(msg) {
|
||||
// The key in localStorage is "[<include>, <exclude>]".
|
||||
window.localStorage[JSON.stringify(msg)] = true;
|
||||
});
|
||||
},
|
||||
// The testPageMod callback with test assertions is called on 'end',
|
||||
// and we want this page mod to be attached before it gets called,
|
||||
// so we attach it on 'start'.
|
||||
contentScriptWhen: 'start',
|
||||
onAttach: function(worker) {
|
||||
worker.postMessage([this.include[0], this.exclude[0]]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testPageMod(assert, done, testPageURI, [
|
||||
createPageModTest("*", testPageURI, false),
|
||||
createPageModTest(testPageURI, testPageURI, false),
|
||||
createPageModTest(testPageURI, "resource://*", false),
|
||||
createPageModTest(testPageURI, "*.google.com", true)
|
||||
],
|
||||
function (win, done) {
|
||||
waitUntil(() => win.localStorage[JSON.stringify([testPageURI, "*.google.com"])],
|
||||
testPageURI + " page-mod to be executed")
|
||||
.then(() => {
|
||||
asserts.forEach(fn => fn(assert, win));
|
||||
win.localStorage.clear();
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testPageModValidationAttachTo = function(assert) {
|
||||
|
@ -187,6 +232,27 @@ exports.testPageModValidationInclude = function(assert) {
|
|||
});
|
||||
};
|
||||
|
||||
exports.testPageModValidationExclude = function(assert) {
|
||||
let includeVal = '*.validation111';
|
||||
|
||||
[{ val: {}, type: 'object' },
|
||||
{ val: [], type: 'empty array'},
|
||||
{ val: [/regexp/, 1], type: 'array with non string/regexp' },
|
||||
{ val: 1, type: 'number' }].forEach((exclude) => {
|
||||
assert.throws(() => new PageMod({ include: includeVal, exclude: exclude.val }),
|
||||
/If set, the `exclude` option must always contain at least one rule as a string, regular expression, or an array of strings and regular expressions./,
|
||||
"PageMod() throws when 'exclude' option is " + exclude.type + ".");
|
||||
});
|
||||
|
||||
[{ val: undefined, type: 'undefined' },
|
||||
{ val: '*.validation111', type: 'string' },
|
||||
{ val: /validation111/, type: 'regexp' },
|
||||
{ val: ['*.validation111'], type: 'array with length > 0'}].forEach((exclude) => {
|
||||
new PageMod({ include: includeVal, exclude: exclude.val });
|
||||
assert.pass("PageMod() does not throw when exclude option is " + exclude.type);
|
||||
});
|
||||
};
|
||||
|
||||
/* Tests for internal functions. */
|
||||
exports.testCommunication1 = function(assert, done) {
|
||||
let workerDone = false,
|
||||
|
|
|
@ -38,6 +38,7 @@ exports.testSetGetBool = function(assert) {
|
|||
assert.equal(sp.test, undefined, "Value should not exist");
|
||||
sp.test = true;
|
||||
assert.ok(sp.test, "Value read should be the value previously set");
|
||||
delete sp.test;
|
||||
};
|
||||
|
||||
// TEST: setting and getting preferences with special characters work
|
||||
|
@ -51,6 +52,7 @@ exports.testSpecialChars = function(assert, done) {
|
|||
simplePrefs.on(char, function onPrefChanged() {
|
||||
simplePrefs.removeListener(char, onPrefChanged);
|
||||
assert.equal(sp[char], rand, "setting pref with a name that is a special char, " + char + ", worked!");
|
||||
delete sp[char];
|
||||
|
||||
// end test
|
||||
if (++count == len)
|
||||
|
@ -64,6 +66,7 @@ exports.testSetGetInt = function(assert) {
|
|||
assert.equal(sp["test-int"], undefined, "Value should not exist");
|
||||
sp["test-int"] = 1;
|
||||
assert.equal(sp["test-int"], 1, "Value read should be the value previously set");
|
||||
delete sp["test-int"];
|
||||
};
|
||||
|
||||
exports.testSetComplex = function(assert) {
|
||||
|
@ -80,6 +83,7 @@ exports.testSetGetString = function(assert) {
|
|||
assert.equal(sp["test-string"], undefined, "Value should not exist");
|
||||
sp["test-string"] = "test";
|
||||
assert.equal(sp["test-string"], "test", "Value read should be the value previously set");
|
||||
delete sp["test-string"];
|
||||
};
|
||||
|
||||
exports.testHasAndRemove = function(assert) {
|
||||
|
@ -93,6 +97,7 @@ exports.testPrefListener = function(assert, done) {
|
|||
let listener = function(prefName) {
|
||||
simplePrefs.removeListener('test-listener', listener);
|
||||
assert.equal(prefName, "test-listen", "The prefs listener heard the right event");
|
||||
delete sp["test-listen"];
|
||||
done();
|
||||
};
|
||||
|
||||
|
@ -112,13 +117,14 @@ exports.testPrefListener = function(assert, done) {
|
|||
|
||||
toSet.forEach(function(pref) {
|
||||
sp[pref] = true;
|
||||
delete sp[pref];
|
||||
});
|
||||
|
||||
assert.ok((observed.length == 3 && toSet.length == 3),
|
||||
assert.ok((observed.length === 6 && toSet.length === 3),
|
||||
"Wildcard lengths inconsistent" + JSON.stringify([observed.length, toSet.length]));
|
||||
|
||||
toSet.forEach(function(pref,ii) {
|
||||
assert.equal(observed[ii], pref, "Wildcard observed " + pref);
|
||||
assert.equal(observed[2*ii], pref, "Wildcard observed " + pref);
|
||||
});
|
||||
|
||||
simplePrefs.removeListener('',wildlistener);
|
||||
|
@ -150,6 +156,7 @@ exports.testPrefRemoveListener = function(assert, done) {
|
|||
|
||||
setTimeout(function() {
|
||||
assert.pass("The prefs listener was removed");
|
||||
delete sp["test-listen2"];
|
||||
done();
|
||||
}, 250);
|
||||
};
|
||||
|
@ -176,6 +183,7 @@ exports.testPrefUnloadListener = function(assert, done) {
|
|||
// this should execute, but also definitely shouldn't fire listener
|
||||
require("sdk/simple-prefs").prefs["test-listen3"] = false;
|
||||
|
||||
delete sp.prefs["test-listen3"];
|
||||
done();
|
||||
};
|
||||
|
||||
|
@ -203,6 +211,7 @@ exports.testPrefUnloadWildcardListener = function(assert, done) {
|
|||
// this should execute, but also definitely shouldn't fire listener
|
||||
require("sdk/simple-prefs").prefs[testpref] = false;
|
||||
|
||||
delete sp.prefs[testpref];
|
||||
done();
|
||||
};
|
||||
|
||||
|
|
|
@ -35,6 +35,12 @@
|
|||
label="New Non-e10s Window"
|
||||
hidden="true"
|
||||
command="Tools:NonRemoteWindow"/>
|
||||
#ifdef MAC_NON_BROWSER_WINDOW
|
||||
<menuitem id="menu_openLocation"
|
||||
label="&openLocationCmd.label;"
|
||||
command="Browser:OpenLocation"
|
||||
key="focusURLBar"/>
|
||||
#endif
|
||||
<menuitem id="menu_openFile"
|
||||
label="&openFileCmd.label;"
|
||||
command="Browser:OpenFile"
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
# 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/.
|
||||
|
||||
#define MAC_NON_BROWSER_WINDOW
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ function checkPlacesContextMenu(aItemWithContextMenu) {
|
|||
return Task.spawn(function* () {
|
||||
let contextMenu = document.getElementById("placesContext");
|
||||
let newBookmarkItem = document.getElementById("placesContext_new:bookmark");
|
||||
info("Waiting for context menu on " + aItemWithContextMenu.id);
|
||||
let shownPromise = popupShown(contextMenu);
|
||||
EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu,
|
||||
{type: "contextmenu", button: 2});
|
||||
|
@ -49,6 +50,7 @@ function checkPlacesContextMenu(aItemWithContextMenu) {
|
|||
ok(!newBookmarkItem.hasAttribute("disabled"),
|
||||
"New bookmark item shouldn't be disabled");
|
||||
|
||||
info("Closing context menu");
|
||||
yield closePopup(contextMenu);
|
||||
});
|
||||
}
|
||||
|
@ -81,14 +83,17 @@ function checkSpecialContextMenus() {
|
|||
for (let menuID in kSpecialItemIDs) {
|
||||
let menuItem = document.getElementById(menuID);
|
||||
let menuPopup = document.getElementById(kSpecialItemIDs[menuID]);
|
||||
info("Waiting to open menu for " + menuID);
|
||||
let shownPromise = popupShown(menuPopup);
|
||||
EventUtils.synthesizeMouseAtCenter(menuItem, {});
|
||||
yield shownPromise;
|
||||
|
||||
yield checkPlacesContextMenu(menuPopup);
|
||||
info("Closing menu for " + menuID);
|
||||
yield closePopup(menuPopup);
|
||||
}
|
||||
|
||||
info("Closing bookmarks menu");
|
||||
yield closePopup(bookmarksMenuPopup);
|
||||
});
|
||||
}
|
||||
|
@ -116,6 +121,7 @@ function checkBookmarksItemsChevronContextMenu() {
|
|||
let shownPromise = popupShown(chevronPopup);
|
||||
let chevron = document.getElementById("PlacesChevron");
|
||||
EventUtils.synthesizeMouseAtCenter(chevron, {});
|
||||
info("Waiting for bookmark toolbar item chevron popup to show");
|
||||
yield shownPromise;
|
||||
yield waitForCondition(() => {
|
||||
for (let child of chevronPopup.children) {
|
||||
|
@ -124,6 +130,7 @@ function checkBookmarksItemsChevronContextMenu() {
|
|||
}
|
||||
});
|
||||
yield checkPlacesContextMenu(chevronPopup);
|
||||
info("Waiting for bookmark toolbar item chevron popup to close");
|
||||
yield closePopup(chevronPopup);
|
||||
});
|
||||
}
|
||||
|
@ -134,6 +141,7 @@ function checkBookmarksItemsChevronContextMenu() {
|
|||
* overflowable nav-bar is showing its chevron.
|
||||
*/
|
||||
function overflowEverything() {
|
||||
info("Waiting for overflow");
|
||||
window.resizeTo(kSmallWidth, window.outerHeight);
|
||||
return waitForCondition(() => gNavBar.hasAttribute("overflowing"));
|
||||
}
|
||||
|
@ -144,6 +152,7 @@ function overflowEverything() {
|
|||
* overflowing.
|
||||
*/
|
||||
function stopOverflowing() {
|
||||
info("Waiting until we stop overflowing");
|
||||
window.resizeTo(kOriginalWindowWidth, window.outerHeight);
|
||||
return waitForCondition(() => !gNavBar.hasAttribute("overflowing"));
|
||||
}
|
||||
|
@ -202,6 +211,7 @@ add_task(function* testOverflowingBookmarksButtonContextMenu() {
|
|||
* to the menu from the overflow panel, and then back to the toolbar.
|
||||
*/
|
||||
add_task(function* testOverflowingBookmarksItemsContextMenu() {
|
||||
info("Ensuring panel is ready.");
|
||||
yield PanelUI.ensureReady();
|
||||
|
||||
let bookmarksToolbarItems = document.getElementById(kBookmarksItems);
|
||||
|
@ -235,6 +245,7 @@ add_task(function* testOverflowingBookmarksItemsChevronContextMenu() {
|
|||
let placesToolbarItems = document.getElementById("PlacesToolbarItems");
|
||||
let placesChevron = document.getElementById("PlacesChevron");
|
||||
placesToolbarItems.style.maxWidth = "10px";
|
||||
info("Waiting for chevron to no longer be collapsed");
|
||||
yield waitForCondition(() => !placesChevron.collapsed);
|
||||
|
||||
yield checkBookmarksItemsChevronContextMenu();
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Advanced panel -->
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/advanced.js"/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Applications panel -->
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/applications.js"/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Content panel -->
|
||||
|
||||
<preferences id="contentPreferences">
|
||||
|
||||
|
|
|
@ -5,17 +5,10 @@
|
|||
browser.jar:
|
||||
content/browser/preferences/in-content/preferences.js
|
||||
* content/browser/preferences/in-content/preferences.xul
|
||||
* content/browser/preferences/in-content/main.xul
|
||||
* content/browser/preferences/in-content/main.js
|
||||
content/browser/preferences/in-content/privacy.xul
|
||||
* content/browser/preferences/in-content/privacy.js
|
||||
* content/browser/preferences/in-content/advanced.xul
|
||||
* content/browser/preferences/in-content/advanced.js
|
||||
content/browser/preferences/in-content/applications.xul
|
||||
* content/browser/preferences/in-content/applications.js
|
||||
content/browser/preferences/in-content/content.xul
|
||||
content/browser/preferences/in-content/content.js
|
||||
content/browser/preferences/in-content/sync.xul
|
||||
content/browser/preferences/in-content/sync.js
|
||||
content/browser/preferences/in-content/security.xul
|
||||
content/browser/preferences/in-content/security.js
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- General panel -->
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/main.js"/>
|
||||
|
|
|
@ -32,12 +32,6 @@ function init_all() {
|
|||
|
||||
let categories = document.getElementById("categories");
|
||||
categories.addEventListener("select", event => gotoPref(event.target.value));
|
||||
|
||||
if (history.length > 1 && history.state) {
|
||||
selectCategory(history.state);
|
||||
} else {
|
||||
history.replaceState("paneGeneral", document.title);
|
||||
}
|
||||
}
|
||||
|
||||
function selectCategory(name) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Privacy panel -->
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/privacy.js"/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Security panel -->
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://browser/content/preferences/in-content/security.js"/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
# 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/.
|
||||
|
||||
<!-- Synch panel -->
|
||||
|
||||
<preferences>
|
||||
<preference id="engine.addons"
|
||||
|
|
|
@ -2134,18 +2134,22 @@ let SessionStoreInternal = {
|
|||
recentCrashes: this._recentCrashes
|
||||
};
|
||||
|
||||
// get open Scratchpad window states too
|
||||
let scratchpads = ScratchpadManager.getSessionState();
|
||||
|
||||
let state = {
|
||||
windows: total,
|
||||
selectedWindow: ix + 1,
|
||||
_closedWindows: lastClosedWindowsCopy,
|
||||
session: session,
|
||||
scratchpads: scratchpads,
|
||||
global: this._globalState.getState()
|
||||
};
|
||||
|
||||
if (Cu.isModuleLoaded("resource:///modules/devtools/scratchpad-manager.jsm")) {
|
||||
// get open Scratchpad window states too
|
||||
let scratchpads = ScratchpadManager.getSessionState();
|
||||
if (scratchpads && scratchpads.length) {
|
||||
state.scratchpads = scratchpads;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the last session if we deferred restoring it
|
||||
if (LastSession.canRestore) {
|
||||
state.lastSessionState = LastSession.getState();
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<xul:deck anonid="translationStates" selectedIndex="0">
|
||||
|
||||
<!-- offer to translate -->
|
||||
<xul:hbox class="translate-offer-box" align="baseline">
|
||||
<xul:hbox class="translate-offer-box" align="center">
|
||||
<xul:label value="&translation.thisPageIsIn.label;"/>
|
||||
<xul:menulist anonid="detectedLanguage">
|
||||
<xul:menupopup/>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</xul:vbox>
|
||||
|
||||
<!-- translated -->
|
||||
<xul:hbox class="translated-box" align="baseline">
|
||||
<xul:hbox class="translated-box" align="center">
|
||||
<xul:label value="&translation.translatedFrom.label;"/>
|
||||
<xul:menulist anonid="fromLanguage"
|
||||
oncommand="document.getBindingParent(this).translate()">
|
||||
|
@ -64,7 +64,7 @@
|
|||
</xul:hbox>
|
||||
|
||||
<!-- error -->
|
||||
<xul:hbox class="translation-error" align="baseline">
|
||||
<xul:hbox class="translation-error" align="center">
|
||||
<xul:label value="&translation.errorTranslating.label;"/>
|
||||
<xul:button label="&translation.tryAgain.button;" anonid="tryAgain"
|
||||
oncommand="document.getBindingParent(this).translate();"/>
|
||||
|
|
|
@ -44,9 +44,12 @@ const EVENTS = {
|
|||
BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
|
||||
BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
|
||||
|
||||
// When a breakpoint has been shown or hidden in the source editor.
|
||||
BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
|
||||
BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
|
||||
// When a breakpoint has been shown or hidden in the source editor
|
||||
// or the pane.
|
||||
BREAKPOINT_SHOWN_IN_EDITOR: "Debugger:BreakpointShownInEditor",
|
||||
BREAKPOINT_SHOWN_IN_PANE: "Debugger:BreakpointShownInPane",
|
||||
BREAKPOINT_HIDDEN_IN_EDITOR: "Debugger:BreakpointHiddenInEditor",
|
||||
BREAKPOINT_HIDDEN_IN_PANE: "Debugger:BreakpointHiddenInPane",
|
||||
|
||||
// When a conditional breakpoint's popup is showing or hiding.
|
||||
CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
|
||||
|
@ -68,6 +71,9 @@ const EVENTS = {
|
|||
GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
|
||||
GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
|
||||
|
||||
// After the the StackFrames object has been filled with frames
|
||||
AFTER_FRAMES_REFILLED: "Debugger:AfterFramesRefilled",
|
||||
|
||||
// After the stackframes are cleared and debugger won't pause anymore.
|
||||
AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
|
||||
|
||||
|
@ -90,7 +96,6 @@ const FRAME_TYPE = {
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
|
||||
Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
|
||||
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
|
||||
|
@ -105,6 +110,9 @@ const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
|
|||
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
|
||||
const FastListWidget = require("devtools/shared/widgets/FastListWidget");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Parser",
|
||||
"resource:///modules/devtools/Parser.jsm");
|
||||
|
||||
|
@ -147,13 +155,14 @@ let DebuggerController = {
|
|||
* @return object
|
||||
* A promise that is resolved when the debugger finishes startup.
|
||||
*/
|
||||
startupDebugger: function() {
|
||||
startupDebugger: Task.async(function*() {
|
||||
if (this._startup) {
|
||||
return this._startup;
|
||||
return;
|
||||
}
|
||||
|
||||
return this._startup = DebuggerView.initialize();
|
||||
},
|
||||
yield DebuggerView.initialize();
|
||||
this._startup = true;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Destroys the view and disconnects the debugger client from the server.
|
||||
|
@ -161,20 +170,20 @@ let DebuggerController = {
|
|||
* @return object
|
||||
* A promise that is resolved when the debugger finishes shutdown.
|
||||
*/
|
||||
shutdownDebugger: function() {
|
||||
shutdownDebugger: Task.async(function*() {
|
||||
if (this._shutdown) {
|
||||
return this._shutdown;
|
||||
return;
|
||||
}
|
||||
|
||||
return this._shutdown = DebuggerView.destroy().then(() => {
|
||||
DebuggerView.destroy();
|
||||
this.SourceScripts.disconnect();
|
||||
this.StackFrames.disconnect();
|
||||
this.ThreadState.disconnect();
|
||||
this.Tracer.disconnect();
|
||||
this.disconnect();
|
||||
});
|
||||
},
|
||||
yield DebuggerView.destroy();
|
||||
this.SourceScripts.disconnect();
|
||||
this.StackFrames.disconnect();
|
||||
this.ThreadState.disconnect();
|
||||
this.Tracer.disconnect();
|
||||
this.disconnect();
|
||||
|
||||
this._shutdown = true;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Initiates remote debugging based on the current target, wiring event
|
||||
|
@ -183,14 +192,11 @@ let DebuggerController = {
|
|||
* @return object
|
||||
* A promise that is resolved when the debugger finishes connecting.
|
||||
*/
|
||||
connect: function() {
|
||||
if (this._connection) {
|
||||
return this._connection;
|
||||
connect: Task.async(function*() {
|
||||
if (this._connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
let startedDebugging = promise.defer();
|
||||
this._connection = startedDebugging.promise;
|
||||
|
||||
let target = this._target;
|
||||
let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
|
||||
target.on("close", this._onTabDetached);
|
||||
|
@ -199,23 +205,17 @@ let DebuggerController = {
|
|||
this.client = client;
|
||||
|
||||
if (addonActor) {
|
||||
this._startAddonDebugging(addonActor, startedDebugging.resolve);
|
||||
yield this._startAddonDebugging(addonActor);
|
||||
} else if (target.chrome) {
|
||||
this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
|
||||
yield this._startChromeDebugging(chromeDebugger);
|
||||
} else {
|
||||
this._startDebuggingTab(startedDebugging.resolve);
|
||||
const startedTracing = promise.defer();
|
||||
yield this._startDebuggingTab();
|
||||
|
||||
if (Prefs.tracerEnabled && traceActor) {
|
||||
this._startTracingTab(traceActor, startedTracing.resolve);
|
||||
} else {
|
||||
startedTracing.resolve();
|
||||
yield this._startTracingTab(traceActor);
|
||||
}
|
||||
|
||||
return promise.all([startedDebugging.promise, startedTracing.promise]);
|
||||
}
|
||||
|
||||
return startedDebugging.promise;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Disconnects the debugger client and removes event handlers as necessary.
|
||||
|
@ -226,7 +226,7 @@ let DebuggerController = {
|
|||
return;
|
||||
}
|
||||
|
||||
this._connection = null;
|
||||
this._connected = false;
|
||||
this.client = null;
|
||||
this.activeThread = null;
|
||||
},
|
||||
|
@ -286,30 +286,33 @@ let DebuggerController = {
|
|||
/**
|
||||
* Sets up a debugging session.
|
||||
*
|
||||
* @param function aCallback
|
||||
* A function to invoke once the client attaches to the active thread.
|
||||
* @return object
|
||||
* A promise resolved once the client attaches to the active thread.
|
||||
*/
|
||||
_startDebuggingTab: function(aCallback) {
|
||||
this._target.activeTab.attachThread({
|
||||
_startDebuggingTab: function() {
|
||||
let deferred = promise.defer();
|
||||
let threadOptions = {
|
||||
useSourceMaps: Prefs.sourceMapsEnabled
|
||||
}, (aResponse, aThreadClient) => {
|
||||
};
|
||||
|
||||
this._target.activeTab.attachThread(threadOptions, (aResponse, aThreadClient) => {
|
||||
if (!aThreadClient) {
|
||||
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
|
||||
deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
|
||||
return;
|
||||
}
|
||||
this.activeThread = aThreadClient;
|
||||
|
||||
this.ThreadState.connect();
|
||||
this.StackFrames.connect();
|
||||
this.SourceScripts.connect();
|
||||
|
||||
if (aThreadClient.paused) {
|
||||
aThreadClient.resume(this._ensureResumptionOrder);
|
||||
}
|
||||
|
||||
if (aCallback) {
|
||||
aCallback();
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -317,13 +320,17 @@ let DebuggerController = {
|
|||
*
|
||||
* @param object aAddonActor
|
||||
* The actor for the addon that is being debugged.
|
||||
* @param function aCallback
|
||||
* A function to invoke once the client attaches to the active thread.
|
||||
* @return object
|
||||
* A promise resolved once the client attaches to the active thread.
|
||||
*/
|
||||
_startAddonDebugging: function(aAddonActor, aCallback) {
|
||||
this.client.attachAddon(aAddonActor, (aResponse) => {
|
||||
return this._startChromeDebugging(aResponse.threadActor, aCallback);
|
||||
_startAddonDebugging: function(aAddonActor) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
this.client.attachAddon(aAddonActor, aResponse => {
|
||||
this._startChromeDebugging(aResponse.threadActor).then(deferred.resolve);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -331,28 +338,33 @@ let DebuggerController = {
|
|||
*
|
||||
* @param object aChromeDebugger
|
||||
* The remote protocol grip of the chrome debugger.
|
||||
* @param function aCallback
|
||||
* A function to invoke once the client attaches to the active thread.
|
||||
* @return object
|
||||
* A promise resolved once the client attaches to the active thread.
|
||||
*/
|
||||
_startChromeDebugging: function(aChromeDebugger, aCallback) {
|
||||
_startChromeDebugging: function(aChromeDebugger) {
|
||||
let deferred = promise.defer();
|
||||
let threadOptions = {
|
||||
useSourceMaps: Prefs.sourceMapsEnabled
|
||||
};
|
||||
|
||||
this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
|
||||
if (!aThreadClient) {
|
||||
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
|
||||
deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
|
||||
return;
|
||||
}
|
||||
this.activeThread = aThreadClient;
|
||||
|
||||
this.ThreadState.connect();
|
||||
this.StackFrames.connect();
|
||||
this.SourceScripts.connect();
|
||||
|
||||
if (aThreadClient.paused) {
|
||||
aThreadClient.resume(this._ensureResumptionOrder);
|
||||
}
|
||||
|
||||
if (aCallback) {
|
||||
aCallback();
|
||||
}
|
||||
}, { useSourceMaps: Prefs.sourceMapsEnabled });
|
||||
deferred.resolve();
|
||||
}, threadOptions);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -360,24 +372,24 @@ let DebuggerController = {
|
|||
*
|
||||
* @param object aTraceActor
|
||||
* The remote protocol grip of the trace actor.
|
||||
* @param function aCallback
|
||||
* A function to invoke once the client attaches to the tracer.
|
||||
* @return object
|
||||
* A promise resolved once the client attaches to the tracer.
|
||||
*/
|
||||
_startTracingTab: function(aTraceActor, aCallback) {
|
||||
_startTracingTab: function(aTraceActor) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
this.client.attachTracer(aTraceActor, (response, traceClient) => {
|
||||
if (!traceClient) {
|
||||
DevToolsUtils.reportException("DebuggerController._startTracingTab",
|
||||
new Error("Failed to attach to tracing actor."));
|
||||
deferred.reject(new Error("Failed to attach to tracing actor."));
|
||||
return;
|
||||
}
|
||||
|
||||
this.traceClient = traceClient;
|
||||
this.Tracer.connect();
|
||||
|
||||
if (aCallback) {
|
||||
aCallback();
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -405,9 +417,9 @@ let DebuggerController = {
|
|||
});
|
||||
},
|
||||
|
||||
_startup: null,
|
||||
_shutdown: null,
|
||||
_connection: null,
|
||||
_startup: false,
|
||||
_shutdown: false,
|
||||
_connected: false,
|
||||
client: null,
|
||||
activeThread: null
|
||||
};
|
||||
|
@ -599,79 +611,21 @@ StackFrames.prototype = {
|
|||
/**
|
||||
* Handler for the thread client's framesadded notification.
|
||||
*/
|
||||
_onFrames: function() {
|
||||
_onFrames: Task.async(function*() {
|
||||
// Ignore useless notifications.
|
||||
if (!this.activeThread || !this.activeThread.cachedFrames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let waitForNextPause = false;
|
||||
let breakLocation = this._currentBreakpointLocation;
|
||||
let watchExpressions = this._currentWatchExpressions;
|
||||
let client = DebuggerController.activeThread.client;
|
||||
|
||||
// We moved conditional breakpoint handling to the server, but
|
||||
// need to support it in the client for a while until most of the
|
||||
// server code in production is updated with it. bug 990137 is
|
||||
// filed to mark this code to be removed.
|
||||
if (!client.mainRoot.traits.conditionalBreakpoints) {
|
||||
// Conditional breakpoints are { breakpoint, expression } tuples. The
|
||||
// boolean evaluation of the expression decides if the active thread
|
||||
// automatically resumes execution or not.
|
||||
if (breakLocation) {
|
||||
// Make sure a breakpoint actually exists at the specified url and line.
|
||||
let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
|
||||
if (breakpointPromise) {
|
||||
breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
|
||||
// Evaluating the current breakpoint's conditional expression will
|
||||
// cause the stack frames to be cleared and active thread to pause,
|
||||
// sending a 'clientEvaluated' packed and adding the frames again.
|
||||
this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
|
||||
waitForNextPause = true;
|
||||
}});
|
||||
}
|
||||
}
|
||||
// We'll get our evaluation of the current breakpoint's conditional
|
||||
// expression the next time the thread client pauses...
|
||||
if (waitForNextPause) {
|
||||
return;
|
||||
}
|
||||
if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
|
||||
this._currentFrameDescription = FRAME_TYPE.NORMAL;
|
||||
// If the breakpoint's conditional expression evaluation is falsy,
|
||||
// automatically resume execution.
|
||||
if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
|
||||
this.activeThread.resume(DebuggerController._ensureResumptionOrder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch expressions are evaluated in the context of the topmost frame,
|
||||
// and the results are displayed in the variables view.
|
||||
// TODO: handle all of this server-side: Bug 832470, comment 14.
|
||||
if (watchExpressions) {
|
||||
// Evaluation causes the stack frames to be cleared and active thread to
|
||||
// pause, sending a 'clientEvaluated' packet and adding the frames again.
|
||||
this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
|
||||
waitForNextPause = true;
|
||||
}
|
||||
// We'll get our evaluation of the current watch expressions the next time
|
||||
// the thread client pauses...
|
||||
if (waitForNextPause) {
|
||||
if (this._currentFrameDescription != FRAME_TYPE.NORMAL &&
|
||||
this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
|
||||
return;
|
||||
}
|
||||
if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
|
||||
this._currentFrameDescription = FRAME_TYPE.NORMAL;
|
||||
// If an error was thrown during the evaluation of the watch expressions,
|
||||
// then at least one expression evaluation could not be performed. So
|
||||
// remove the most recent watch expression and try again.
|
||||
if (this._currentEvaluation.throw) {
|
||||
DebuggerView.WatchExpressions.removeAt(0);
|
||||
DebuggerController.StackFrames.syncWatchExpressions();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove all of this deprecated code: Bug 990137.
|
||||
yield this._handleConditionalBreakpoint();
|
||||
|
||||
// TODO: handle all of this server-side: Bug 832470, comment 14.
|
||||
yield this._handleWatchExpressions();
|
||||
|
||||
// Make sure the debugger view panes are visible, then refill the frames.
|
||||
DebuggerView.showInstrumentsPane();
|
||||
|
@ -681,7 +635,7 @@ StackFrames.prototype = {
|
|||
if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
|
||||
this._currentFrameDescription = FRAME_TYPE.NORMAL;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fill the StackFrames view with the frames we have in the cache, compressing
|
||||
|
@ -690,7 +644,6 @@ StackFrames.prototype = {
|
|||
_refillFrames: function() {
|
||||
// Make sure all the previous stackframes are removed before re-adding them.
|
||||
DebuggerView.StackFrames.empty();
|
||||
|
||||
for (let frame of this.activeThread.cachedFrames) {
|
||||
let { depth, where: { url, line }, source } = frame;
|
||||
let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
|
||||
|
@ -701,6 +654,8 @@ StackFrames.prototype = {
|
|||
|
||||
DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
|
||||
DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
|
||||
|
||||
window.emit(EVENTS.AFTER_FRAMES_REFILLED);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -743,14 +698,16 @@ StackFrames.prototype = {
|
|||
* Handler for the debugger's prettyprintchange notification.
|
||||
*/
|
||||
_onPrettyPrintChange: function() {
|
||||
if (this.activeThread.state != "paused") {
|
||||
return;
|
||||
}
|
||||
// Makes sure the selected source remains selected
|
||||
// after the fillFrames is called.
|
||||
const source = DebuggerView.Sources.selectedValue;
|
||||
if (this.activeThread.state == "paused") {
|
||||
this.activeThread.fillFrames(
|
||||
CALL_STACK_PAGE_SIZE,
|
||||
() => DebuggerView.Sources.selectedValue = source);
|
||||
}
|
||||
|
||||
this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => {
|
||||
DebuggerView.Sources.selectedValue = source;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -938,6 +895,87 @@ StackFrames.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles conditional breakpoints when the debugger pauses and the
|
||||
* stackframes are received.
|
||||
*
|
||||
* We moved conditional breakpoint handling to the server, but
|
||||
* need to support it in the client for a while until most of the
|
||||
* server code in production is updated with it.
|
||||
* TODO: remove all of this deprecated code: Bug 990137.
|
||||
*
|
||||
* @return object
|
||||
* A promise that is resolved after a potential breakpoint's
|
||||
* conditional expression is evaluated. If there's no breakpoint
|
||||
* where the debugger is paused, the promise is resolved immediately.
|
||||
*/
|
||||
_handleConditionalBreakpoint: Task.async(function*() {
|
||||
if (gClient.mainRoot.traits.conditionalBreakpoints) {
|
||||
return;
|
||||
}
|
||||
let breakLocation = this._currentBreakpointLocation;
|
||||
if (!breakLocation) {
|
||||
return;
|
||||
}
|
||||
let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
|
||||
if (!breakpointPromise) {
|
||||
return;
|
||||
}
|
||||
let breakpointClient = yield breakpointPromise;
|
||||
let conditionalExpression = breakpointClient.conditionalExpression;
|
||||
if (!conditionalExpression) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluating the current breakpoint's conditional expression will
|
||||
// cause the stack frames to be cleared and active thread to pause,
|
||||
// sending a 'clientEvaluated' packed and adding the frames again.
|
||||
let evaluationOptions = { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL };
|
||||
yield this.evaluate(conditionalExpression, evaluationOptions);
|
||||
this._currentFrameDescription = FRAME_TYPE.NORMAL;
|
||||
|
||||
// If the breakpoint's conditional expression evaluation is falsy,
|
||||
// automatically resume execution.
|
||||
if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
|
||||
this.activeThread.resume(DebuggerController._ensureResumptionOrder);
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Handles watch expressions when the debugger pauses and the stackframes
|
||||
* are received.
|
||||
*
|
||||
* @return object
|
||||
* A promise that is resolved after the potential watch expressions
|
||||
* are evaluated. If there are no watch expressions where the debugger
|
||||
* is paused, the promise is resolved immediately.
|
||||
*/
|
||||
_handleWatchExpressions: Task.async(function*() {
|
||||
// Ignore useless notifications.
|
||||
if (!this.activeThread || !this.activeThread.cachedFrames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let watchExpressions = this._currentWatchExpressions;
|
||||
if (!watchExpressions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluation causes the stack frames to be cleared and active thread to
|
||||
// pause, sending a 'clientEvaluated' packet and adding the frames again.
|
||||
let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL };
|
||||
yield this.evaluate(watchExpressions, evaluationOptions);
|
||||
this._currentFrameDescription = FRAME_TYPE.NORMAL;
|
||||
|
||||
// If an error was thrown during the evaluation of the watch expressions,
|
||||
// then at least one expression evaluation could not be performed. So
|
||||
// remove the most recent watch expression and try again.
|
||||
if (this._currentEvaluation.throw) {
|
||||
DebuggerView.WatchExpressions.removeAt(0);
|
||||
yield DebuggerController.StackFrames.syncWatchExpressions();
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Adds the watch expressions evaluation results to a scope in the view.
|
||||
*
|
||||
|
@ -998,30 +1036,29 @@ StackFrames.prototype = {
|
|||
}
|
||||
});
|
||||
|
||||
if (sanitizedExpressions.length) {
|
||||
this._syncedWatchExpressions =
|
||||
this._currentWatchExpressions =
|
||||
"[" +
|
||||
sanitizedExpressions.map(aString =>
|
||||
"eval(\"" +
|
||||
"try {" +
|
||||
// Make sure all quotes are escaped in the expression's syntax,
|
||||
// and add a newline after the statement to avoid comments
|
||||
// breaking the code integrity inside the eval block.
|
||||
aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
|
||||
"} catch (e) {" +
|
||||
"e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
|
||||
"}" +
|
||||
"\")"
|
||||
).join(",") +
|
||||
"]";
|
||||
if (!sanitizedExpressions.length) {
|
||||
this._currentWatchExpressions = null;
|
||||
this._syncedWatchExpressions = null;
|
||||
} else {
|
||||
this._syncedWatchExpressions =
|
||||
this._currentWatchExpressions = null;
|
||||
this._currentWatchExpressions = "[" +
|
||||
sanitizedExpressions.map(aString =>
|
||||
"eval(\"" +
|
||||
"try {" +
|
||||
// Make sure all quotes are escaped in the expression's syntax,
|
||||
// and add a newline after the statement to avoid comments
|
||||
// breaking the code integrity inside the eval block.
|
||||
aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
|
||||
"} catch (e) {" +
|
||||
"e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
|
||||
"}" +
|
||||
"\")"
|
||||
).join(",") +
|
||||
"]";
|
||||
}
|
||||
|
||||
this.currentFrameDepth = -1;
|
||||
this._onFrames();
|
||||
return this._onFrames();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1322,13 +1359,12 @@ SourceScripts.prototype = {
|
|||
}
|
||||
|
||||
// Get the source text from the active thread.
|
||||
this.activeThread.source(aSource)
|
||||
.source(({ error, message, source: text, contentType }) => {
|
||||
this.activeThread.source(aSource).source(({ error, source: text, contentType }) => {
|
||||
if (aOnTimeout) {
|
||||
window.clearTimeout(fetchTimeout);
|
||||
}
|
||||
if (error) {
|
||||
deferred.reject([aSource, message || error]);
|
||||
deferred.reject([aSource, error]);
|
||||
} else {
|
||||
deferred.resolve([aSource, text, contentType]);
|
||||
}
|
||||
|
@ -1441,12 +1477,14 @@ Tracer.prototype = {
|
|||
* Instructs the tracer actor to start tracing.
|
||||
*/
|
||||
startTracing: function(aCallback = () => {}) {
|
||||
DebuggerView.Tracer.selectTab();
|
||||
if (this.tracing) {
|
||||
return;
|
||||
}
|
||||
this._trace = "dbg.trace" + Math.random();
|
||||
this.traceClient.startTrace([
|
||||
|
||||
DebuggerView.Tracer.selectTab();
|
||||
|
||||
let id = this._trace = "dbg.trace" + Math.random();
|
||||
let fields = [
|
||||
"name",
|
||||
"location",
|
||||
"parameterNames",
|
||||
|
@ -1455,7 +1493,9 @@ Tracer.prototype = {
|
|||
"return",
|
||||
"throw",
|
||||
"yield"
|
||||
], this._trace, (aResponse) => {
|
||||
];
|
||||
|
||||
this.traceClient.startTrace(fields, id, aResponse => {
|
||||
const { error } = aResponse;
|
||||
if (error) {
|
||||
DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
|
||||
|
@ -1487,11 +1527,11 @@ Tracer.prototype = {
|
|||
onTraces: function (aEvent, { traces }) {
|
||||
const tracesLength = traces.length;
|
||||
let tracesToShow;
|
||||
|
||||
if (tracesLength > TracerView.MAX_TRACES) {
|
||||
tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
|
||||
tracesLength);
|
||||
DebuggerView.Tracer.empty();
|
||||
tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, tracesLength);
|
||||
this._stack.splice(0, this._stack.length);
|
||||
DebuggerView.Tracer.empty();
|
||||
} else {
|
||||
tracesToShow = traces;
|
||||
}
|
||||
|
@ -1614,7 +1654,6 @@ Tracer.prototype = {
|
|||
* Handles breaking on event listeners in the currently debugged target.
|
||||
*/
|
||||
function EventListeners() {
|
||||
this._onEventListeners = this._onEventListeners.bind(this);
|
||||
}
|
||||
|
||||
EventListeners.prototype = {
|
||||
|
@ -1642,65 +1681,71 @@ EventListeners.prototype = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Fetches the currently attached event listeners from the debugee.
|
||||
* Schedules fetching the currently attached event listeners from the debugee.
|
||||
*/
|
||||
scheduleEventListenersFetch: function() {
|
||||
let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
|
||||
if (aResponse.error) {
|
||||
let msg = "Error getting event listeners: " + aResponse.message;
|
||||
DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
|
||||
const deferred = promise.defer();
|
||||
|
||||
gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
|
||||
if (aResponse.error) {
|
||||
const msg = "Error getting function definition site: " + aResponse.message;
|
||||
DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
|
||||
} else {
|
||||
aListener.function.url = aResponse.url;
|
||||
}
|
||||
|
||||
deferred.resolve(aListener);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
promise.all(outstandingListenersDefinitionSite).then(aListeners => {
|
||||
this._onEventListeners(aListeners);
|
||||
|
||||
// Notify that event listeners were fetched and shown in the view,
|
||||
// and callback to resume the active thread if necessary.
|
||||
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
aCallback && aCallback();
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure we're not sending a batch of closely repeated requests.
|
||||
// This can easily happen whenever new sources are fetched.
|
||||
setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
|
||||
if (gThreadClient.state != "paused") {
|
||||
gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
|
||||
gThreadClient.interrupt(() => this._getListeners(() => gThreadClient.resume()));
|
||||
} else {
|
||||
getListeners();
|
||||
this._getListeners();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback for a debugger's successful active thread eventListeners() call.
|
||||
* Fetches the currently attached event listeners from the debugee.
|
||||
* The thread client state is assumed to be "paused".
|
||||
*
|
||||
* @param function aCallback
|
||||
* Invoked once the event listeners are fetched and displayed.
|
||||
*/
|
||||
_onEventListeners: function(aListeners) {
|
||||
// Add all the listeners in the debugger view event linsteners container.
|
||||
for (let listener of aListeners) {
|
||||
DebuggerView.EventListeners.addListener(listener, { staged: true });
|
||||
}
|
||||
_getListeners: function(aCallback) {
|
||||
gThreadClient.eventListeners(Task.async(function*(aResponse) {
|
||||
if (aResponse.error) {
|
||||
throw "Error getting event listeners: " + aResponse.message;
|
||||
}
|
||||
|
||||
// Flushes all the prepared events into the event listeners container.
|
||||
DebuggerView.EventListeners.commit();
|
||||
// Add all the listeners in the debugger view event linsteners container.
|
||||
for (let listener of aResponse.listeners) {
|
||||
let definitionSite = yield this._getDefinitionSite(listener.function);
|
||||
listener.function.url = definitionSite;
|
||||
DebuggerView.EventListeners.addListener(listener, { staged: true });
|
||||
}
|
||||
|
||||
// Flushes all the prepared events into the event listeners container.
|
||||
DebuggerView.EventListeners.commit();
|
||||
|
||||
// Notify that event listeners were fetched and shown in the view,
|
||||
// and callback to resume the active thread if necessary.
|
||||
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
aCallback && aCallback();
|
||||
}.bind(this)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a function's source-mapped definiton site.
|
||||
*
|
||||
* @param object aFunction
|
||||
* The grip of the function to get the definition site for.
|
||||
* @return object
|
||||
* A promise that is resolved with the function's owner source url,
|
||||
* or rejected if an error occured.
|
||||
*/
|
||||
_getDefinitionSite: function(aFunction) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => {
|
||||
if (aResponse.error) {
|
||||
deferred.reject("Error getting function definition site: " + aResponse.message);
|
||||
} else {
|
||||
deferred.resolve(aResponse.url);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1756,9 +1801,10 @@ Breakpoints.prototype = {
|
|||
* @param number aLine
|
||||
* Line number where breakpoint was set.
|
||||
*/
|
||||
_onEditorBreakpointAdd: function(_, aLine) {
|
||||
_onEditorBreakpointAdd: Task.async(function*(_, aLine) {
|
||||
let url = DebuggerView.Sources.selectedValue;
|
||||
let location = { url: url, line: aLine + 1 };
|
||||
let breakpointClient = yield this.addBreakpoint(location, { noEditorUpdate: true });
|
||||
|
||||
// Initialize the breakpoint, but don't update the editor, since this
|
||||
// callback is invoked because a breakpoint was added in the editor itself.
|
||||
|
@ -1766,16 +1812,16 @@ Breakpoints.prototype = {
|
|||
// If the breakpoint client has a "requestedLocation" attached, then
|
||||
// the original requested placement for the breakpoint wasn't accepted.
|
||||
// In this case, we need to update the editor with the new location.
|
||||
if (aBreakpointClient.requestedLocation) {
|
||||
if (breakpointClient.requestedLocation) {
|
||||
DebuggerView.editor.moveBreakpoint(
|
||||
aBreakpointClient.requestedLocation.line - 1,
|
||||
aBreakpointClient.location.line - 1
|
||||
breakpointClient.requestedLocation.line - 1,
|
||||
breakpointClient.location.line - 1
|
||||
);
|
||||
}
|
||||
// Notify that we've shown a breakpoint in the source editor.
|
||||
window.emit(EVENTS.BREAKPOINT_SHOWN);
|
||||
window.emit(EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Event handler for breakpoints that are removed from the editor.
|
||||
|
@ -1783,17 +1829,14 @@ Breakpoints.prototype = {
|
|||
* @param number aLine
|
||||
* Line number where breakpoint was removed.
|
||||
*/
|
||||
_onEditorBreakpointRemove: function(_, aLine) {
|
||||
_onEditorBreakpointRemove: Task.async(function*(_, aLine) {
|
||||
let url = DebuggerView.Sources.selectedValue;
|
||||
let location = { url: url, line: aLine + 1 };
|
||||
yield this.removeBreakpoint(location, { noEditorUpdate: true });
|
||||
|
||||
// Destroy the breakpoint, but don't update the editor, since this callback
|
||||
// is invoked because a breakpoint was removed from the editor itself.
|
||||
this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
|
||||
// Notify that we've hidden a breakpoint in the source editor.
|
||||
window.emit(EVENTS.BREAKPOINT_HIDDEN);
|
||||
});
|
||||
},
|
||||
// Notify that we've hidden a breakpoint in the source editor.
|
||||
window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_EDITOR);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the breakpoints in the editor view. This function takes the list of
|
||||
|
@ -1801,19 +1844,18 @@ Breakpoints.prototype = {
|
|||
* This is invoked when the selected script is changed, or when new sources
|
||||
* are received via the _onNewSource and _onSourcesAdded event listeners.
|
||||
*/
|
||||
updateEditorBreakpoints: function() {
|
||||
updateEditorBreakpoints: Task.async(function*() {
|
||||
for (let breakpointPromise of this._addedOrDisabled) {
|
||||
breakpointPromise.then(aBreakpointClient => {
|
||||
let currentSourceUrl = DebuggerView.Sources.selectedValue;
|
||||
let breakpointUrl = aBreakpointClient.location.url;
|
||||
let breakpointClient = yield breakpointPromise;
|
||||
let currentSourceUrl = DebuggerView.Sources.selectedValue;
|
||||
let breakpointUrl = breakpointClient.location.url;
|
||||
|
||||
// Update the view only if the breakpoint is in the currently shown source.
|
||||
if (currentSourceUrl == breakpointUrl) {
|
||||
this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
|
||||
}
|
||||
});
|
||||
// Update the view only if the breakpoint is in the currently shown source.
|
||||
if (currentSourceUrl == breakpointUrl) {
|
||||
this._showBreakpoint(breakpointClient, { noPaneUpdate: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the breakpoints in the pane view. This function takes the list of
|
||||
|
@ -1821,19 +1863,18 @@ Breakpoints.prototype = {
|
|||
* This is invoked when new sources are received via the _onNewSource and
|
||||
* _onSourcesAdded event listeners.
|
||||
*/
|
||||
updatePaneBreakpoints: function() {
|
||||
updatePaneBreakpoints: Task.async(function*() {
|
||||
for (let breakpointPromise of this._addedOrDisabled) {
|
||||
breakpointPromise.then(aBreakpointClient => {
|
||||
let container = DebuggerView.Sources;
|
||||
let breakpointUrl = aBreakpointClient.location.url;
|
||||
let breakpointClient = yield breakpointPromise;
|
||||
let container = DebuggerView.Sources;
|
||||
let breakpointUrl = breakpointClient.location.url;
|
||||
|
||||
// Update the view only if the breakpoint exists in a known source.
|
||||
if (container.containsValue(breakpointUrl)) {
|
||||
this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
|
||||
}
|
||||
});
|
||||
// Update the view only if the breakpoint exists in a known source.
|
||||
if (container.containsValue(breakpointUrl)) {
|
||||
this._showBreakpoint(breakpointClient, { noEditorUpdate: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Add a breakpoint.
|
||||
|
@ -2048,22 +2089,19 @@ Breakpoints.prototype = {
|
|||
* @return object
|
||||
* A promise that will be resolved with the breakpoint client
|
||||
*/
|
||||
updateCondition: function(aLocation, aCondition) {
|
||||
updateCondition: Task.async(function*(aLocation, aCondition) {
|
||||
let addedPromise = this._getAdded(aLocation);
|
||||
if (!addedPromise) {
|
||||
return promise.reject(new Error('breakpoint does not exist ' +
|
||||
'in specified location'));
|
||||
throw new Error("Breakpoint does not exist at the specified location");
|
||||
}
|
||||
|
||||
var promise = addedPromise.then(aBreakpointClient => {
|
||||
return aBreakpointClient.setCondition(gThreadClient, aCondition);
|
||||
});
|
||||
let breakpointClient = yield addedPromise;
|
||||
let promise = breakpointClient.setCondition(gThreadClient, aCondition);
|
||||
|
||||
// `setCondition` returns a new breakpoint that has the condition,
|
||||
// so we need to update the store
|
||||
this._added.set(this.getIdentifier(aLocation), promise);
|
||||
return promise;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the editor and breakpoints pane to show a specified breakpoint.
|
||||
|
|
|
@ -3,12 +3,8 @@
|
|||
/* 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";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
// Used to detect minification for automatic pretty printing
|
||||
const SAMPLE_SIZE = 50; // no of lines
|
||||
const INDENT_COUNT_THRESHOLD = 5; // percentage
|
||||
|
@ -41,8 +37,6 @@ function SourcesView() {
|
|||
this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
|
||||
this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
|
||||
this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
|
||||
|
||||
this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
|
||||
}
|
||||
|
||||
SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
|
@ -56,15 +50,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
showArrows: true
|
||||
});
|
||||
|
||||
// Sort known source groups towards the end of the list
|
||||
this.widget.groupSortPredicate = function(a, b) {
|
||||
if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
|
||||
};
|
||||
|
||||
this.emptyText = L10N.getStr("noSourcesText");
|
||||
this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
|
||||
|
||||
|
@ -98,6 +83,14 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
return +(aFirst.attachment.label.toLowerCase() >
|
||||
aSecond.attachment.label.toLowerCase());
|
||||
});
|
||||
|
||||
// Sort known source groups towards the end of the list
|
||||
this.widget.groupSortPredicate = function(a, b) {
|
||||
if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -216,6 +209,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
if (aOptions.openPopup || !aOptions.noEditorUpdate) {
|
||||
this.highlightBreakpoint(location, aOptions);
|
||||
}
|
||||
|
||||
window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -239,6 +234,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
|
||||
// Clear the breakpoint view.
|
||||
sourceItem.remove(breakpointItem);
|
||||
|
||||
window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -429,8 +426,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
* Unhighlights the current breakpoint in this sources container.
|
||||
*/
|
||||
unhighlightBreakpoint: function() {
|
||||
this._unselectBreakpoint();
|
||||
this._hideConditionalPopup();
|
||||
this._unselectBreakpoint();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -459,7 +456,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
/**
|
||||
* Toggle the pretty printing of the selected source.
|
||||
*/
|
||||
togglePrettyPrint: function() {
|
||||
togglePrettyPrint: Task.async(function*() {
|
||||
if (this._prettyPrintButton.hasAttribute("disabled")) {
|
||||
return;
|
||||
}
|
||||
|
@ -486,25 +483,29 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
this._prettyPrintButton.removeAttribute("checked");
|
||||
}
|
||||
|
||||
DebuggerController.SourceScripts.togglePrettyPrint(source)
|
||||
.then(resetEditor, printError)
|
||||
.then(DebuggerView.showEditor)
|
||||
.then(this.updateToolbarButtonsState);
|
||||
},
|
||||
try {
|
||||
let resolution = yield DebuggerController.SourceScripts.togglePrettyPrint(source);
|
||||
resetEditor(resolution);
|
||||
} catch (rejection) {
|
||||
printError(rejection);
|
||||
}
|
||||
|
||||
DebuggerView.showEditor();
|
||||
this.updateToolbarButtonsState();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Toggle the black boxed state of the selected source.
|
||||
*/
|
||||
toggleBlackBoxing: function() {
|
||||
toggleBlackBoxing: Task.async(function*() {
|
||||
const { source } = this.selectedItem.attachment;
|
||||
const sourceClient = gThreadClient.source(source);
|
||||
const shouldBlackBox = !sourceClient.isBlackBoxed;
|
||||
|
||||
// Be optimistic that the (un-)black boxing will succeed, so enable/disable
|
||||
// the pretty print button and check/uncheck the black box button
|
||||
// immediately. Then, once we actually get the results from the server, make
|
||||
// sure that it is in the correct state again by calling
|
||||
// `updateToolbarButtonsState`.
|
||||
// the pretty print button and check/uncheck the black box button immediately.
|
||||
// Then, once we actually get the results from the server, make sure that
|
||||
// it is in the correct state again by calling `updateToolbarButtonsState`.
|
||||
|
||||
if (shouldBlackBox) {
|
||||
this._prettyPrintButton.setAttribute("disabled", true);
|
||||
|
@ -514,10 +515,14 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
this._blackBoxButton.removeAttribute("checked");
|
||||
}
|
||||
|
||||
DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
|
||||
.then(this.updateToolbarButtonsState,
|
||||
this.updateToolbarButtonsState);
|
||||
},
|
||||
try {
|
||||
yield DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox);
|
||||
} catch (e) {
|
||||
// Continue execution in this task even if blackboxing failed.
|
||||
}
|
||||
|
||||
this.updateToolbarButtonsState();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Toggles all breakpoints enabled/disabled.
|
||||
|
@ -806,7 +811,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
/**
|
||||
* The select listener for the sources container.
|
||||
*/
|
||||
_onSourceSelect: function({ detail: sourceItem }) {
|
||||
_onSourceSelect: Task.async(function*({ detail: sourceItem }) {
|
||||
if (!sourceItem) {
|
||||
return;
|
||||
}
|
||||
|
@ -816,12 +821,12 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
// The container is not empty and an actual item was selected.
|
||||
DebuggerView.setEditorLocation(sourceItem.value);
|
||||
|
||||
// Attempt to automatically pretty print minified source code.
|
||||
if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) {
|
||||
DebuggerController.SourceScripts.getText(source).then(([, aText]) => {
|
||||
if (SourceUtils.isMinified(sourceClient, aText)) {
|
||||
this.togglePrettyPrint();
|
||||
}
|
||||
}).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e));
|
||||
let isMinified = yield SourceUtils.isMinified(sourceClient);
|
||||
if (isMinified) {
|
||||
this.togglePrettyPrint();
|
||||
}
|
||||
}
|
||||
|
||||
// Set window title. No need to split the url by " -> " here, because it was
|
||||
|
@ -830,18 +835,22 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
|
||||
DebuggerView.maybeShowBlackBoxMessage();
|
||||
this.updateToolbarButtonsState();
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* The click listener for the "stop black boxing" button.
|
||||
*/
|
||||
_onStopBlackBoxing: function() {
|
||||
_onStopBlackBoxing: Task.async(function*() {
|
||||
const { source } = this.selectedItem.attachment;
|
||||
|
||||
DebuggerController.SourceScripts.setBlackBoxing(source, false)
|
||||
.then(this.updateToolbarButtonsState,
|
||||
this.updateToolbarButtonsState);
|
||||
},
|
||||
try {
|
||||
yield DebuggerController.SourceScripts.setBlackBoxing(source, false);
|
||||
} catch (e) {
|
||||
// Continue execution in this task even if blackboxing failed.
|
||||
}
|
||||
|
||||
this.updateToolbarButtonsState();
|
||||
}),
|
||||
|
||||
/**
|
||||
* The click listener for a breakpoint container.
|
||||
|
@ -913,6 +922,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
*/
|
||||
_onConditionalPopupHiding: Task.async(function*() {
|
||||
this._conditionalPopupVisible = false; // Used in tests.
|
||||
|
||||
let breakpointItem = this._selectedBreakpointItem;
|
||||
let attachment = breakpointItem.attachment;
|
||||
|
||||
|
@ -920,11 +930,9 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
// save the current conditional epression.
|
||||
let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
|
||||
if (breakpointPromise) {
|
||||
let breakpointClient = yield breakpointPromise;
|
||||
yield DebuggerController.Breakpoints.updateCondition(
|
||||
breakpointClient.location,
|
||||
this._cbTextbox.value
|
||||
);
|
||||
let { location } = yield breakpointPromise;
|
||||
let condition = this._cbTextbox.value;
|
||||
yield DebuggerController.Breakpoints.updateCondition(location, condition);
|
||||
}
|
||||
|
||||
window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
|
||||
|
@ -1124,7 +1132,8 @@ function TracerView() {
|
|||
DevToolsUtils.makeInfallible(this._onSelect.bind(this));
|
||||
this._onMouseOver =
|
||||
DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
|
||||
this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
|
||||
this._onSearch =
|
||||
DevToolsUtils.makeInfallible(this._onSearch.bind(this));
|
||||
}
|
||||
|
||||
TracerView.MAX_TRACES = 200;
|
||||
|
@ -1257,6 +1266,7 @@ TracerView.prototype = Heritage.extend(WidgetMethods, {
|
|||
*/
|
||||
_populateVariable: function(aName, aParent, aValue) {
|
||||
let item = aParent.addItem(aName, { value: aValue });
|
||||
|
||||
if (aValue) {
|
||||
let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue);
|
||||
DebuggerView.Variables.controller.populate(item, wrappedValue);
|
||||
|
@ -1512,16 +1522,15 @@ let SourceUtils = {
|
|||
* Determines if the source text is minified by using
|
||||
* the percentage indented of a subset of lines
|
||||
*
|
||||
* @param string aText
|
||||
* The source text.
|
||||
* @return boolean
|
||||
* True if source text is minified.
|
||||
* @return object
|
||||
* A promise that resolves to true if source text is minified.
|
||||
*/
|
||||
isMinified: function(sourceClient, aText){
|
||||
isMinified: Task.async(function*(sourceClient) {
|
||||
if (this._minifiedCache.has(sourceClient)) {
|
||||
return this._minifiedCache.get(sourceClient);
|
||||
}
|
||||
|
||||
let [, text] = yield DebuggerController.SourceScripts.getText(sourceClient);
|
||||
let isMinified;
|
||||
let lineEndIndex = 0;
|
||||
let lineStartIndex = 0;
|
||||
|
@ -1530,14 +1539,14 @@ let SourceUtils = {
|
|||
let overCharLimit = false;
|
||||
|
||||
// Strip comments.
|
||||
aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
|
||||
text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
|
||||
|
||||
while (lines++ < SAMPLE_SIZE) {
|
||||
lineEndIndex = aText.indexOf("\n", lineStartIndex);
|
||||
lineEndIndex = text.indexOf("\n", lineStartIndex);
|
||||
if (lineEndIndex == -1) {
|
||||
break;
|
||||
}
|
||||
if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) {
|
||||
if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
|
||||
indentCount++;
|
||||
}
|
||||
// For files with no indents but are not minified.
|
||||
|
@ -1547,12 +1556,13 @@ let SourceUtils = {
|
|||
}
|
||||
lineStartIndex = lineEndIndex + 1;
|
||||
}
|
||||
isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD ||
|
||||
overCharLimit;
|
||||
|
||||
isMinified =
|
||||
((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
|
||||
|
||||
this._minifiedCache.set(sourceClient, isMinified);
|
||||
return isMinified;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Clears the labels, groups and minify cache, populated by methods like
|
||||
|
@ -1590,6 +1600,7 @@ let SourceUtils = {
|
|||
if (!sourceLabel) {
|
||||
sourceLabel = this.trimUrl(aUrl);
|
||||
}
|
||||
|
||||
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
|
||||
this._labelsCache.set(aUrl, unicodeLabel);
|
||||
return unicodeLabel;
|
||||
|
@ -2328,12 +2339,11 @@ WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
|
|||
* The keypress listener for a watch expression's textbox.
|
||||
*/
|
||||
_onKeyPress: function(e) {
|
||||
switch(e.keyCode) {
|
||||
switch (e.keyCode) {
|
||||
case e.DOM_VK_RETURN:
|
||||
case e.DOM_VK_ESCAPE:
|
||||
e.stopPropagation();
|
||||
DebuggerView.editor.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -2404,6 +2414,7 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
|||
let eventItem = this.getItemForPredicate(aItem =>
|
||||
aItem.attachment.url == url &&
|
||||
aItem.attachment.type == type);
|
||||
|
||||
if (eventItem) {
|
||||
let { selectors, view: { targets } } = eventItem.attachment;
|
||||
if (selectors.indexOf(selector) == -1) {
|
||||
|
|
|
@ -120,6 +120,10 @@ ToolbarView.prototype = {
|
|||
* Listener handling the pause/resume button click event.
|
||||
*/
|
||||
_onResumePressed: function() {
|
||||
if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DebuggerController.activeThread.paused) {
|
||||
let warn = DebuggerController._ensureResumptionOrder;
|
||||
DebuggerController.StackFrames.currentFrameDepth = -1;
|
||||
|
@ -144,6 +148,10 @@ ToolbarView.prototype = {
|
|||
* Listener handling the step in button click event.
|
||||
*/
|
||||
_onStepInPressed: function() {
|
||||
if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DebuggerController.activeThread.paused) {
|
||||
DebuggerController.StackFrames.currentFrameDepth = -1;
|
||||
let warn = DebuggerController._ensureResumptionOrder;
|
||||
|
@ -315,7 +323,7 @@ OptionsView.prototype = {
|
|||
window.setTimeout(() => {
|
||||
DebuggerController.reconfigureThread(pref);
|
||||
}, POPUP_HIDDEN_DELAY);
|
||||
}, false);
|
||||
});
|
||||
},
|
||||
|
||||
_button: null,
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<command id="globalSearchCommand"
|
||||
oncommand="DebuggerView.Filtering._doGlobalSearch()"/>
|
||||
<command id="functionSearchCommand"
|
||||
oncommand="DepbuggerView.Filtering._doFunctionSearch()"/>
|
||||
oncommand="DebuggerView.Filtering._doFunctionSearch()"/>
|
||||
<command id="tokenSearchCommand"
|
||||
oncommand="DebuggerView.Filtering._doTokenSearch()"/>
|
||||
<command id="lineSearchCommand"
|
||||
|
|
|
@ -59,7 +59,7 @@ function test() {
|
|||
is(gBreakpointsAdded.size, 0, "no breakpoints added");
|
||||
|
||||
let cmd = gContextMenu.querySelector('menuitem[command=addBreakpointCommand]');
|
||||
let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN);
|
||||
let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
|
||||
EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger);
|
||||
return bpShown;
|
||||
}).then(() => {
|
||||
|
|
|
@ -51,7 +51,12 @@ function test() {
|
|||
return resumeAndTestBreakpoint(30);
|
||||
})
|
||||
.then(() => resumeAndTestNoBreakpoint())
|
||||
.then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
|
||||
.then(() => {
|
||||
return promise.all([
|
||||
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
|
||||
]);
|
||||
})
|
||||
.then(() => testAfterReload())
|
||||
.then(() => {
|
||||
// Reset traits back to default value
|
||||
|
|
|
@ -103,7 +103,8 @@ function afterDebuggerStatementHit() {
|
|||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE),
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCES_ADDED),
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN),
|
||||
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN)
|
||||
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR),
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE)
|
||||
]).then(testClickAgain);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,12 @@ function test() {
|
|||
.then(() => resumeAndTestBreakpoint(28))
|
||||
.then(() => resumeAndTestBreakpoint(29))
|
||||
.then(() => resumeAndTestNoBreakpoint())
|
||||
.then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
|
||||
.then(() => {
|
||||
return promise.all([
|
||||
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
|
||||
]);
|
||||
})
|
||||
.then(() => testAfterReload())
|
||||
.then(() => closeDebuggerAndFinish(gPanel))
|
||||
.then(null, aError => {
|
||||
|
|
|
@ -19,7 +19,9 @@ function test() {
|
|||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, ".html", 26).then(performTest);
|
||||
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
|
||||
.then(performTest);
|
||||
|
||||
gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
|
||||
gDebuggee.recurse();
|
||||
|
@ -32,15 +34,15 @@ function performTest() {
|
|||
is(gFrames.itemCount, gDebugger.gCallStackPageSize,
|
||||
"Should have only the max limit of frames.");
|
||||
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize,
|
||||
"Should have only the max limit of frames in the mirrored view as well.")
|
||||
"Should have only the max limit of frames in the mirrored view as well.");
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
|
||||
is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2,
|
||||
"Should now have twice the max limit of frames.");
|
||||
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
|
||||
"Should now have twice the max limit of frames in the mirrored view as well.");
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
|
||||
is(gFrames.itemCount, gDebuggee.gRecurseLimit,
|
||||
"Should have reached the recurse limit.");
|
||||
is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit,
|
||||
|
|
|
@ -25,9 +25,9 @@ function test() {
|
|||
gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
|
||||
|
||||
waitForSourceShown(gPanel, ".html", 1)
|
||||
.then(() => addExpressions())
|
||||
.then(() => performTest())
|
||||
.then(() => finishTest())
|
||||
.then(addExpressions)
|
||||
.then(performTest)
|
||||
.then(finishTest)
|
||||
.then(() => closeDebuggerAndFinish(gPanel))
|
||||
.then(null, aError => {
|
||||
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
|
||||
|
|
|
@ -11,12 +11,19 @@ const DBG_XUL = "chrome://browser/content/devtools/framework/toolbox-process-win
|
|||
const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm")
|
||||
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
let require = devtools.require;
|
||||
let Telemetry = require("devtools/shared/telemetry");
|
||||
let EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsLoader",
|
||||
"resource://gre/modules/devtools/Loader.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
|
||||
"resource://gre/modules/devtools/Loader.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
|
||||
return devtools.require("devtools/shared/telemetry");
|
||||
});
|
||||
XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
|
||||
return devtools.require("devtools/toolkit/event-emitter");
|
||||
});
|
||||
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
|
||||
|
@ -133,10 +140,12 @@ BrowserToolboxProcess.prototype = {
|
|||
dumpn("initialized and added the browser actors for the DebuggerServer.");
|
||||
}
|
||||
|
||||
this.debuggerServer.openListener(Prefs.chromeDebuggingPort);
|
||||
let chromeDebuggingPort =
|
||||
Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
|
||||
this.debuggerServer.openListener(chromeDebuggingPort);
|
||||
|
||||
dumpn("Finished initializing the chrome toolbox server.");
|
||||
dumpn("Started listening on port: " + Prefs.chromeDebuggingPort);
|
||||
dumpn("Started listening on port: " + chromeDebuggingPort);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -247,14 +256,6 @@ BrowserToolboxProcess.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcuts for accessing various debugger preferences.
|
||||
*/
|
||||
let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
|
||||
chromeDebuggingHost: ["Char", "chrome-debugging-host"],
|
||||
chromeDebuggingPort: ["Int", "chrome-debugging-port"]
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper method for debugging.
|
||||
* @param string
|
||||
|
@ -270,3 +271,5 @@ let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
|
|||
Services.prefs.addObserver("devtools.debugger.log", {
|
||||
observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
|
||||
}, false);
|
||||
|
||||
Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);
|
||||
|
|
|
@ -10,10 +10,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
|
||||
|
||||
const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
|
||||
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
|
||||
const MAX_ORDINAL = 99;
|
||||
|
||||
|
|
|
@ -49,7 +49,15 @@ function connect() {
|
|||
});
|
||||
}
|
||||
|
||||
window.addEventListener("load", connect);
|
||||
window.addEventListener("load", function() {
|
||||
let cmdClose = document.getElementById("toolbox-cmd-close");
|
||||
cmdClose.addEventListener("command", onCloseCommand);
|
||||
connect();
|
||||
});
|
||||
|
||||
function onCloseCommand(event) {
|
||||
window.close();
|
||||
}
|
||||
|
||||
function openToolbox(form) {
|
||||
let options = {
|
||||
|
@ -82,6 +90,8 @@ function bindToolboxHandlers() {
|
|||
function onUnload() {
|
||||
window.removeEventListener("unload", onUnload);
|
||||
window.removeEventListener("message", onMessage);
|
||||
let cmdClose = document.getElementById("toolbox-cmd-close");
|
||||
cmdClose.removeEventListener("command", onCloseCommand);
|
||||
gToolbox.destroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<script type="text/javascript" src="chrome://browser/content/utilityOverlay.js"/>
|
||||
|
||||
<commandset id="toolbox-commandset">
|
||||
<command id="toolbox-cmd-close" oncommand="window.close();"/>
|
||||
<command id="toolbox-cmd-close"/>
|
||||
</commandset>
|
||||
|
||||
<keyset id="toolbox-keyset">
|
||||
|
|
|
@ -7,6 +7,11 @@ browser.jar:
|
|||
content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
|
||||
content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml)
|
||||
content/browser/devtools/markup-view.css (markupview/markup-view.css)
|
||||
content/browser/devtools/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
|
||||
content/browser/devtools/readdir.js (projecteditor/lib/helpers/readdir.js)
|
||||
content/browser/devtools/projecteditor-loader.xul (projecteditor/chrome/content/projecteditor-loader.xul)
|
||||
content/browser/devtools/projecteditor-test.html (projecteditor/chrome/content/projecteditor-test.html)
|
||||
content/browser/devtools/projecteditor-loader.js (projecteditor/chrome/content/projecteditor-loader.js)
|
||||
content/browser/devtools/netmonitor.xul (netmonitor/netmonitor.xul)
|
||||
content/browser/devtools/netmonitor.css (netmonitor/netmonitor.css)
|
||||
content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
|
||||
|
|
|
@ -13,6 +13,7 @@ DIRS += [
|
|||
'fontinspector',
|
||||
'framework',
|
||||
'inspector',
|
||||
'projecteditor',
|
||||
'layoutview',
|
||||
'markupview',
|
||||
'netmonitor',
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# 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/.
|
||||
|
||||
projecteditor_lib_FILES = $(wildcard $(srcdir)/lib/*)
|
||||
projecteditor_lib_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor
|
||||
INSTALL_TARGETS += projecteditor_lib
|
||||
|
||||
# To copy the sample directory into modules/devtools/projecteditor
|
||||
# projecteditor_sample_FILES = $(wildcard $(srcdir)/test/samples/*)
|
||||
# projecteditor_sample_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor/samples
|
||||
# INSTALL_TARGETS += projecteditor_sample
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
|
@ -0,0 +1,157 @@
|
|||
const Cu = Components.utils;
|
||||
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
||||
const require = devtools.require;
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const ProjectEditor = require("projecteditor/projecteditor");
|
||||
|
||||
const SAMPLE_PATH = buildTempDirectoryStructure();
|
||||
const SAMPLE_NAME = "DevTools Content";
|
||||
const SAMPLE_PROJECT_URL = "http://mozilla.org";
|
||||
const SAMPLE_ICON = "chrome://browser/skin/devtools/tool-options.svg";
|
||||
|
||||
/**
|
||||
* Create a workspace for working on projecteditor, available at
|
||||
* chrome://browser/content/devtools/projecteditor-loader.xul.
|
||||
* This emulates the integration points that the app manager uses.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", function onDOMReady(e) {
|
||||
document.removeEventListener("DOMContentLoaded", onDOMReady, false);
|
||||
let iframe = document.getElementById("projecteditor-iframe");
|
||||
window.projecteditor = ProjectEditor.ProjectEditor(iframe);
|
||||
|
||||
projecteditor.on("onEditorCreated", (editor) => {
|
||||
console.log("editor created: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorDestroyed", (editor) => {
|
||||
console.log("editor destroyed: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorSave", (editor, resource) => {
|
||||
console.log("editor saved: " + editor, resource.path);
|
||||
});
|
||||
projecteditor.on("onTreeSelected", (resource) => {
|
||||
console.log("tree selected: " + resource.path);
|
||||
});
|
||||
projecteditor.on("onEditorLoad", (editor) => {
|
||||
console.log("editor loaded: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorActivated", (editor) => {
|
||||
console.log("editor focused: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorDeactivated", (editor) => {
|
||||
console.log("editor blur: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorChange", (editor) => {
|
||||
console.log("editor changed: " + editor);
|
||||
});
|
||||
projecteditor.on("onEditorCursorActivity", (editor) => {
|
||||
console.log("editor cursor activity: " + editor);
|
||||
});
|
||||
projecteditor.on("onCommand", (cmd) => {
|
||||
console.log("Command: " + cmd);
|
||||
});
|
||||
|
||||
projecteditor.loaded.then(() => {
|
||||
projecteditor.setProjectToAppPath(SAMPLE_PATH, {
|
||||
name: SAMPLE_NAME,
|
||||
iconUrl: SAMPLE_ICON,
|
||||
projectOverviewURL: SAMPLE_PROJECT_URL
|
||||
}).then(() => {
|
||||
let allResources = projecteditor.project.allResources();
|
||||
console.log("All resources have been loaded", allResources, allResources.map(r=>r.basename).join("|"));
|
||||
});
|
||||
});
|
||||
|
||||
}, false);
|
||||
|
||||
/**
|
||||
* Build a temporary directory as a workspace for this loader
|
||||
* https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
|
||||
*/
|
||||
function buildTempDirectoryStructure() {
|
||||
|
||||
// First create (and remove) the temp dir to discard any changes
|
||||
let TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
|
||||
TEMP_DIR.remove(true);
|
||||
|
||||
// Now rebuild our fake project.
|
||||
TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
|
||||
|
||||
FileUtils.getDir("TmpD", ["ProjectEditor", "css"], true);
|
||||
FileUtils.getDir("TmpD", ["ProjectEditor", "data"], true);
|
||||
FileUtils.getDir("TmpD", ["ProjectEditor", "img", "icons"], true);
|
||||
FileUtils.getDir("TmpD", ["ProjectEditor", "js"], true);
|
||||
|
||||
let htmlFile = FileUtils.getFile("TmpD", ["ProjectEditor", "index.html"]);
|
||||
htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
writeToFile(htmlFile, [
|
||||
'<!DOCTYPE html>',
|
||||
'<html lang="en">',
|
||||
' <head>',
|
||||
' <meta charset="utf-8" />',
|
||||
' <title>ProjectEditor Temp File</title>',
|
||||
' <link rel="stylesheet" href="style.css" />',
|
||||
' </head>',
|
||||
' <body id="home">',
|
||||
' <p>ProjectEditor Temp File</p>',
|
||||
' </body>',
|
||||
'</html>'].join("\n")
|
||||
);
|
||||
|
||||
let readmeFile = FileUtils.getFile("TmpD", ["ProjectEditor", "README.md"]);
|
||||
readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
writeToFile(readmeFile, [
|
||||
'## Readme'
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
let licenseFile = FileUtils.getFile("TmpD", ["ProjectEditor", "LICENSE"]);
|
||||
licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
writeToFile(licenseFile, [
|
||||
'/* 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/. */'
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
let cssFile = FileUtils.getFile("TmpD", ["ProjectEditor", "css", "styles.css"]);
|
||||
cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
writeToFile(cssFile, [
|
||||
'body {',
|
||||
' background: red;',
|
||||
'}'
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||
|
||||
return TEMP_DIR.path;
|
||||
}
|
||||
|
||||
|
||||
// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
|
||||
function writeToFile(file, data) {
|
||||
|
||||
let defer = promise.defer();
|
||||
var ostream = FileUtils.openSafeFileOutputStream(file)
|
||||
|
||||
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
var istream = converter.convertToInputStream(data);
|
||||
|
||||
// The last argument (the callback) is optional.
|
||||
NetUtil.asyncCopy(istream, ostream, function(status) {
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
// Handle error!
|
||||
console.log("ERROR WRITING TEMP FILE", status);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
|
||||
%toolboxDTD;
|
||||
]>
|
||||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
|
||||
<script type="application/javascript;version=1.8" src="projecteditor-loader.js"></script>
|
||||
|
||||
<commandset id="toolbox-commandset">
|
||||
<command id="projecteditor-cmd-close" oncommand="window.close();"/>
|
||||
</commandset>
|
||||
|
||||
<keyset id="projecteditor-keyset">
|
||||
<key id="projecteditor-key-close"
|
||||
key="&closeCmd.key;"
|
||||
command="projecteditor-cmd-close"
|
||||
modifiers="accel"/>
|
||||
</keyset>
|
||||
|
||||
<iframe id="projecteditor-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
|
||||
</window>
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
</head>
|
||||
<body>
|
||||
<style type="text/css">
|
||||
html { height: 100%; }
|
||||
body {display: flex; padding: 0; margin: 0; min-height: 100%; }
|
||||
iframe {flex: 1; border: 0;}
|
||||
</style>
|
||||
<iframe id='projecteditor-iframe'></iframe>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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/. -->
|
||||
<?xml-stylesheet href="chrome://browser/skin/devtools/light-theme.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/devtools/projecteditor/projecteditor.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/content/devtools/markup-view.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/devtools/markup-view.css" type="text/css"?>
|
||||
|
||||
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
|
||||
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
|
||||
%scratchpadDTD;
|
||||
<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
|
||||
%editMenuStrings;
|
||||
<!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
|
||||
%sourceEditorStrings;
|
||||
]>
|
||||
|
||||
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="theme-body">
|
||||
|
||||
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
|
||||
|
||||
<commandset id="projecteditor-commandset" />
|
||||
<commandset id="editMenuCommands"/>
|
||||
<keyset id="projecteditor-keyset" />
|
||||
<keyset id="editMenuKeys"/>
|
||||
|
||||
<!-- Eventually we want to let plugins declare their own menu items.
|
||||
Wait unti app manager lands to deal with this integration point.
|
||||
-->
|
||||
<menubar id="projecteditor-menubar">
|
||||
<menu id="file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
|
||||
<menupopup id="file-menu-popup" />
|
||||
</menu>
|
||||
|
||||
<menu id="edit-menu" label="&editMenu.label;"
|
||||
accesskey="&editMenu.accesskey;">
|
||||
<menupopup id="edit-menu-popup">
|
||||
<menuitem id="menu_undo"/>
|
||||
<menuitem id="menu_redo"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_cut"/>
|
||||
<menuitem id="menu_copy"/>
|
||||
<menuitem id="menu_paste"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_selectAll"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_find"/>
|
||||
<menuitem id="menu_findAgain"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
</menubar>
|
||||
|
||||
|
||||
<popupset>
|
||||
<menupopup id="directory-menu-popup">
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<deck id="main-deck" flex="1">
|
||||
<vbox flex="1" id="source-deckitem">
|
||||
<hbox id="sources-body" flex="1">
|
||||
<vbox width="250">
|
||||
<vbox id="sources" flex="1">
|
||||
</vbox>
|
||||
<toolbar id="project-toolbar" class="devtools-toolbar" hidden="true"></toolbar>
|
||||
</vbox>
|
||||
<splitter id="source-editor-splitter" class="devtools-side-splitter"/>
|
||||
<vbox id="shells" flex="4">
|
||||
<toolbar id="projecteditor-toolbar" class="devtools-toolbar">
|
||||
<hbox id="plugin-toolbar-left"/>
|
||||
<spacer flex="1"/>
|
||||
<hbox id="plugin-toolbar-right"/>
|
||||
</toolbar>
|
||||
<box id="shells-deck-container" flex="4"></box>
|
||||
<toolbar id="projecteditor-toolbar-bottom" class="devtools-toolbar">
|
||||
</toolbar>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</deck>
|
||||
</page>
|
|
@ -0,0 +1,263 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
/**
|
||||
* ItchEditor is extended to implement an editor, which is the main view
|
||||
* that shows up when a file is selected. This object should not be used
|
||||
* directly - use TextEditor for a basic code editor.
|
||||
*/
|
||||
var ItchEditor = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* A boolean specifying if the toolbar above the editor should be hidden.
|
||||
*/
|
||||
hidesToolbar: false,
|
||||
|
||||
toString: function() {
|
||||
return this.label || "";
|
||||
},
|
||||
|
||||
emit: function(name, ...args) {
|
||||
emit(this, name, ...args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the editor with a single document. This should be called
|
||||
* by objects extending this object with:
|
||||
* ItchEditor.prototype.initialize.apply(this, arguments)
|
||||
*/
|
||||
initialize: function(document) {
|
||||
this.doc = document;
|
||||
this.label = "";
|
||||
this.elt = this.doc.createElement("vbox");
|
||||
this.elt.setAttribute("flex", "1");
|
||||
this.elt.editor = this;
|
||||
this.toolbar = this.doc.querySelector("#projecteditor-toolbar");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the visibility of the element that shows up above the editor
|
||||
* based on the this.hidesToolbar property.
|
||||
*/
|
||||
setToolbarVisibility: function() {
|
||||
if (this.hidesToolbar) {
|
||||
this.toolbar.setAttribute("hidden", "true");
|
||||
} else {
|
||||
this.toolbar.removeAttribute("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Load a single resource into the editor.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The single file / item that is being dealt with (see stores/base)
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the editor has loaded the contents
|
||||
* of the resource.
|
||||
*/
|
||||
load: function(resource) {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up the editor. This can have different meanings
|
||||
* depending on the type of editor.
|
||||
*/
|
||||
destroy: function() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Give focus to the editor. This can have different meanings
|
||||
* depending on the type of editor.
|
||||
*
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the editor has been focused.
|
||||
*/
|
||||
focus: function() {
|
||||
return promise.resolve();
|
||||
}
|
||||
});
|
||||
exports.ItchEditor = ItchEditor;
|
||||
|
||||
/**
|
||||
* The main implementation of the ItchEditor class. The TextEditor is used
|
||||
* when editing any sort of plain text file, and can be created with different
|
||||
* modes for syntax highlighting depending on the language.
|
||||
*/
|
||||
var TextEditor = Class({
|
||||
extends: ItchEditor,
|
||||
|
||||
/**
|
||||
* Extra keyboard shortcuts to use with the editor. Shortcuts defined
|
||||
* within projecteditor should be triggered when they happen in the editor, and
|
||||
* they would usually be swallowed without registering them.
|
||||
* See "devtools/sourceeditor/editor" for more information.
|
||||
*/
|
||||
get extraKeys() {
|
||||
let extraKeys = {};
|
||||
|
||||
// Copy all of the registered keys into extraKeys object, to notify CodeMirror
|
||||
// that it should be ignoring these keys
|
||||
[...this.doc.querySelectorAll("#projecteditor-keyset key")].forEach((key) => {
|
||||
let keyUpper = key.getAttribute("key").toUpperCase();
|
||||
let toolModifiers = key.getAttribute("modifiers");
|
||||
let modifiers = {
|
||||
alt: toolModifiers.contains("alt"),
|
||||
shift: toolModifiers.contains("shift")
|
||||
};
|
||||
|
||||
// On the key press, we will dispatch the event within projecteditor.
|
||||
extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
|
||||
let event = this.doc.createEvent('Event');
|
||||
event.initEvent('command', true, true);
|
||||
let command = this.doc.querySelector("#" + key.getAttribute("command"));
|
||||
command.dispatchEvent(event);
|
||||
};
|
||||
});
|
||||
|
||||
return extraKeys;
|
||||
},
|
||||
|
||||
initialize: function(document, mode=Editor.modes.text) {
|
||||
ItchEditor.prototype.initialize.apply(this, arguments);
|
||||
this.label = mode.name;
|
||||
this.editor = new Editor({
|
||||
mode: mode,
|
||||
lineNumbers: true,
|
||||
extraKeys: this.extraKeys,
|
||||
themeSwitching: false
|
||||
});
|
||||
|
||||
// Trigger editor specific events on `this`
|
||||
this.editor.on("change", (...args) => {
|
||||
this.emit("change", ...args);
|
||||
});
|
||||
this.editor.on("cursorActivity", (...args) => {
|
||||
this.emit("cursorActivity", ...args);
|
||||
});
|
||||
|
||||
this.appended = this.editor.appendTo(this.elt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up the editor. This can have different meanings
|
||||
* depending on the type of editor.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.editor.destroy();
|
||||
this.editor = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a single resource into the text editor.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The single file / item that is being dealt with (see stores/base)
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the text editor has loaded the
|
||||
* contents of the resource.
|
||||
*/
|
||||
load: function(resource) {
|
||||
// Wait for the editor.appendTo and resource.load before proceeding.
|
||||
// They can run in parallel.
|
||||
return promise.all([
|
||||
resource.load(),
|
||||
this.appended
|
||||
]).then(([resourceContents])=> {
|
||||
this.editor.setText(resourceContents);
|
||||
this.editor.setClean();
|
||||
this.emit("load");
|
||||
}, console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the resource based on the current state of the editor
|
||||
*
|
||||
* @param Resource resource
|
||||
* The single file / item to be saved
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the resource has been
|
||||
* saved.
|
||||
*/
|
||||
save: function(resource) {
|
||||
return resource.save(this.editor.getText()).then(() => {
|
||||
this.editor.setClean();
|
||||
this.emit("save", resource);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Give focus to the code editor.
|
||||
*
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the editor has been focused.
|
||||
*/
|
||||
focus: function() {
|
||||
return this.appended.then(() => {
|
||||
this.editor.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using JavaScript syntax highlighting.
|
||||
*/
|
||||
function JSEditor(document) {
|
||||
return TextEditor(document, Editor.modes.js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using CSS syntax highlighting.
|
||||
*/
|
||||
function CSSEditor(document) {
|
||||
return TextEditor(document, Editor.modes.css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for TextEditor using HTML syntax highlighting.
|
||||
*/
|
||||
function HTMLEditor(document) {
|
||||
return TextEditor(document, Editor.modes.html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of editor that can handle a particular resource.
|
||||
* @param Resource resource
|
||||
* The single file that is going to be opened.
|
||||
* @returns Type:Editor
|
||||
* The type of editor that can handle this resource. The
|
||||
* return value is a constructor function.
|
||||
*/
|
||||
function EditorTypeForResource(resource) {
|
||||
const categoryMap = {
|
||||
"txt": TextEditor,
|
||||
"html": HTMLEditor,
|
||||
"xml": HTMLEditor,
|
||||
"css": CSSEditor,
|
||||
"js": JSEditor,
|
||||
"json": JSEditor
|
||||
};
|
||||
return categoryMap[resource.contentCategory] || TextEditor;
|
||||
}
|
||||
|
||||
exports.TextEditor = TextEditor;
|
||||
exports.JSEditor = JSEditor;
|
||||
exports.CSSEditor = CSSEditor;
|
||||
exports.HTMLEditor = HTMLEditor;
|
||||
exports.EditorTypeForResource = EditorTypeForResource;
|
|
@ -0,0 +1,86 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This file wraps EventEmitter objects to provide functions to forget
|
||||
* all events bound on a certain object.
|
||||
*/
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
|
||||
/**
|
||||
* The Scope object is used to keep track of listeners.
|
||||
* This object is not exported.
|
||||
*/
|
||||
var Scope = Class({
|
||||
on: function(target, event, handler) {
|
||||
this.listeners = this.listeners || [];
|
||||
this.listeners.push({
|
||||
target: target,
|
||||
event: event,
|
||||
handler: handler
|
||||
});
|
||||
target.on(event, handler);
|
||||
},
|
||||
|
||||
off: function(t, e, h) {
|
||||
if (!this.listeners) return;
|
||||
this.listeners = this.listeners.filter(({ target, event, handler }) => {
|
||||
return !(target === t && event === e && handler === h);
|
||||
});
|
||||
target.off(event, handler);
|
||||
},
|
||||
|
||||
clear: function(clearTarget) {
|
||||
if (!this.listeners) return;
|
||||
this.listeners = this.listeners.filter(({ target, event, handler }) => {
|
||||
if (target === clearTarget) {
|
||||
target.off(event, handler);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (!this.listeners) return;
|
||||
this.listeners.forEach(({ target, event, handler }) => {
|
||||
target.off(event, handler);
|
||||
});
|
||||
this.listeners = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
var scopes = new WeakMap();
|
||||
function scope(owner) {
|
||||
if (!scopes.has(owner)) {
|
||||
let scope = new Scope(owner);
|
||||
scopes.set(owner, scope);
|
||||
return scope;
|
||||
}
|
||||
return scopes.get(owner);
|
||||
}
|
||||
exports.scope = scope;
|
||||
|
||||
exports.on = function on(owner, target, event, handler) {
|
||||
if (!target) return;
|
||||
scope(owner).on(target, event, handler);
|
||||
}
|
||||
|
||||
exports.off = function off(owner, target, event, handler) {
|
||||
if (!target) return;
|
||||
scope(owner).off(target, event, handler);
|
||||
}
|
||||
|
||||
exports.forget = function forget(owner, target) {
|
||||
scope(owner).clear(target);
|
||||
}
|
||||
|
||||
exports.done = function done(owner) {
|
||||
scope(owner).destroy();
|
||||
scopes.delete(owner);
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This file contains helper functions for showing OS-specific
|
||||
* file and folder pickers.
|
||||
*/
|
||||
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const { getLocalizedString } = require("projecteditor/helpers/l10n");
|
||||
|
||||
/**
|
||||
* Show a file / folder picker.
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
|
||||
*
|
||||
* @param object options
|
||||
* Additional options for setting the source. Supported options:
|
||||
* - directory: string, The path to default opening
|
||||
* - defaultName: string, The filename including extension that
|
||||
* should be suggested to the user as a default
|
||||
* - window: DOMWindow, The filename including extension that
|
||||
* should be suggested to the user as a default
|
||||
* - title: string, The filename including extension that
|
||||
* should be suggested to the user as a default
|
||||
* - mode: int, The type of picker to open.
|
||||
*
|
||||
* @return promise
|
||||
* A promise that is resolved with the full path
|
||||
* after the file has been picked.
|
||||
*/
|
||||
function showPicker(options) {
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
if (options.directory) {
|
||||
try {
|
||||
fp.displayDirectory = FileUtils.File(options.directory);
|
||||
} catch(ex) {
|
||||
console.warn(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.defaultName) {
|
||||
fp.defaultString = options.defaultName;
|
||||
}
|
||||
|
||||
fp.init(options.window, options.title, options.mode);
|
||||
let deferred = promise.defer();
|
||||
fp.open({
|
||||
done: function(res) {
|
||||
if (res === Ci.nsIFilePicker.returnOK || res === Ci.nsIFilePicker.returnReplace) {
|
||||
deferred.resolve(fp.file.path);
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.showPicker = showPicker;
|
||||
|
||||
/**
|
||||
* Show a save dialog
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
|
||||
*
|
||||
* @param object options
|
||||
* Additional options as specified in showPicker
|
||||
*
|
||||
* @return promise
|
||||
* A promise that is resolved when the save dialog has closed
|
||||
*/
|
||||
function showSave(options) {
|
||||
return showPicker(merge({
|
||||
title: getLocalizedString("projecteditor.selectFileLabel"),
|
||||
mode: Ci.nsIFilePicker.modeSave
|
||||
}, options));
|
||||
}
|
||||
exports.showSave = showSave;
|
||||
|
||||
/**
|
||||
* Show a file open dialog
|
||||
*
|
||||
* @param object options
|
||||
* Additional options as specified in showPicker
|
||||
*
|
||||
* @return promise
|
||||
* A promise that is resolved when the file has been opened
|
||||
*/
|
||||
function showOpen(options) {
|
||||
return showPicker(merge({
|
||||
title: getLocalizedString("projecteditor.openFileLabel"),
|
||||
mode: Ci.nsIFilePicker.modeOpen
|
||||
}, options));
|
||||
}
|
||||
exports.showOpen = showOpen;
|
||||
|
||||
/**
|
||||
* Show a folder open dialog
|
||||
*
|
||||
* @param object options
|
||||
* Additional options as specified in showPicker
|
||||
*
|
||||
* @return promise
|
||||
* A promise that is resolved when the folder has been opened
|
||||
*/
|
||||
function showOpenFolder(options) {
|
||||
return showPicker(merge({
|
||||
title: getLocalizedString("projecteditor.openFolderLabel"),
|
||||
mode: Ci.nsIFilePicker.modeGetFolder
|
||||
}, options));
|
||||
}
|
||||
exports.showOpenFolder = showOpenFolder;
|
|
@ -0,0 +1,25 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This file contains helper functions for internationalizing projecteditor strings
|
||||
*/
|
||||
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
|
||||
const ITCHPAD_STRINGS_URI = "chrome://browser/locale/devtools/projecteditor.properties";
|
||||
const L10N = new ViewHelpers.L10N(ITCHPAD_STRINGS_URI).stringBundle;
|
||||
|
||||
function getLocalizedString (name) {
|
||||
try {
|
||||
return L10N.GetStringFromName(name);
|
||||
} catch (ex) {
|
||||
console.log("Error reading '" + name + "'");
|
||||
throw new Error("l10n error with " + name);
|
||||
}
|
||||
}
|
||||
|
||||
exports.getLocalizedString = getLocalizedString;
|
|
@ -0,0 +1,11 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This helper is a quick way to require() the Promise object from Promise.jsm.
|
||||
*/
|
||||
const { Cu } = require("chrome");
|
||||
module.exports = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
importScripts("resource://gre/modules/osfile.jsm");
|
||||
|
||||
/**
|
||||
* This file is meant to be loaded in a worker using:
|
||||
* new ChromeWorker("chrome://browser/content/devtools/readdir.js");
|
||||
*
|
||||
* Read a local directory inside of a web woker
|
||||
*
|
||||
* @param {string} path
|
||||
* window to inspect
|
||||
* @param {RegExp|string} ignore
|
||||
* A pattern to ignore certain files. This is
|
||||
* called with file.name.match(ignore).
|
||||
* @param {Number} maxDepth
|
||||
* How many directories to recurse before stopping.
|
||||
* Directories with depth > maxDepth will be ignored.
|
||||
*/
|
||||
function readDir(path, ignore, maxDepth = Infinity) {
|
||||
let ret = {};
|
||||
|
||||
let set = new Set();
|
||||
|
||||
let info = OS.File.stat(path);
|
||||
set.add({
|
||||
path: path,
|
||||
name: info.name,
|
||||
isDir: info.isDir,
|
||||
isSymLink: info.isSymLink,
|
||||
depth: 0
|
||||
});
|
||||
|
||||
for (let info of set) {
|
||||
let children = [];
|
||||
|
||||
if (info.isDir && !info.isSymLink) {
|
||||
if (info.depth > maxDepth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let iterator = new OS.File.DirectoryIterator(info.path);
|
||||
try {
|
||||
for (let child in iterator) {
|
||||
if (ignore && child.name.match(ignore)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
children.push(child.path);
|
||||
set.add({
|
||||
path: child.path,
|
||||
name: child.name,
|
||||
isDir: child.isDir,
|
||||
isSymLink: child.isSymLink,
|
||||
depth: info.depth + 1
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
iterator.close();
|
||||
}
|
||||
}
|
||||
|
||||
ret[info.path] = {
|
||||
name: info.name,
|
||||
isDir: info.isDir,
|
||||
isSymLink: info.isSymLink,
|
||||
depth: info.depth,
|
||||
children: children,
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
onmessage = function (event) {
|
||||
try {
|
||||
let {path, ignore, depth} = event.data;
|
||||
let message = readDir(path, ignore, depth);
|
||||
postMessage(message);
|
||||
} catch(ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { ItchEditor } = require("projecteditor/editors");
|
||||
|
||||
var AppProjectEditor = Class({
|
||||
extends: ItchEditor,
|
||||
|
||||
hidesToolbar: true,
|
||||
|
||||
initialize: function(document, host) {
|
||||
ItchEditor.prototype.initialize.apply(this, arguments);
|
||||
this.appended = promise.resolve();
|
||||
this.host = host;
|
||||
this.label = "app-manager";
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.elt.remove();
|
||||
this.elt = null;
|
||||
},
|
||||
|
||||
load: function(resource) {
|
||||
this.elt.textContent = "";
|
||||
let {appManagerOpts} = this.host.project;
|
||||
let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
|
||||
iframe.setAttribute("flex", "1");
|
||||
iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
|
||||
this.elt.appendChild(iframe);
|
||||
|
||||
// Wait for other `appended` listeners before emitting load.
|
||||
this.appended.then(() => {
|
||||
this.emit("load");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.AppProjectEditor = AppProjectEditor;
|
|
@ -0,0 +1,47 @@
|
|||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
const { AppProjectEditor } = require("./app-project-editor");
|
||||
|
||||
var AppManagerRenderer = Class({
|
||||
extends: Plugin,
|
||||
|
||||
isAppManagerProject: function() {
|
||||
return !!this.host.project.appManagerOpts;
|
||||
},
|
||||
editorForResource: function(resource) {
|
||||
if (!resource.parent && this.isAppManagerProject()) {
|
||||
return AppProjectEditor;
|
||||
}
|
||||
},
|
||||
onAnnotate: function(resource, editor, elt) {
|
||||
if (resource.parent || !this.isAppManagerProject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {appManagerOpts} = this.host.project;
|
||||
let doc = elt.ownerDocument;
|
||||
let image = doc.createElement("image");
|
||||
let label = doc.createElement("label");
|
||||
|
||||
label.className = "project-name-label";
|
||||
image.className = "project-image";
|
||||
|
||||
let name = appManagerOpts.name || resource.basename;
|
||||
let url = appManagerOpts.iconUrl || "icon-sample.png";
|
||||
|
||||
label.textContent = name;
|
||||
image.setAttribute("src", url);
|
||||
|
||||
elt.innerHTML = "";
|
||||
elt.appendChild(image);
|
||||
elt.appendChild(label);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
exports.AppManagerRenderer = AppManagerRenderer;
|
||||
registerPlugin(AppManagerRenderer);
|
|
@ -0,0 +1,83 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
// This is the core plugin API.
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
|
||||
var Plugin = Class({
|
||||
initialize: function(host) {
|
||||
this.host = host;
|
||||
this.init(host);
|
||||
},
|
||||
|
||||
destroy: function(host) { },
|
||||
|
||||
init: function(host) {},
|
||||
|
||||
showForCategories: function(elt, categories) {
|
||||
this._showFor = this._showFor || [];
|
||||
let set = new Set(categories);
|
||||
this._showFor.push({
|
||||
elt: elt,
|
||||
categories: new Set(categories)
|
||||
});
|
||||
if (this.host.currentEditor) {
|
||||
this.onEditorActivated(this.host.currentEditor);
|
||||
} else {
|
||||
elt.classList.add("plugin-hidden");
|
||||
}
|
||||
},
|
||||
|
||||
priv: function(item) {
|
||||
if (!this._privData) {
|
||||
this._privData = new WeakMap();
|
||||
}
|
||||
if (!this._privData.has(item)) {
|
||||
this._privData.set(item, {});
|
||||
}
|
||||
return this._privData.get(item);
|
||||
},
|
||||
onTreeSelected: function(resource) {},
|
||||
|
||||
|
||||
// Editor state lifetime...
|
||||
onEditorCreated: function(editor) {},
|
||||
onEditorDestroyed: function(editor) {},
|
||||
|
||||
onEditorActivated: function(editor) {
|
||||
if (this._showFor) {
|
||||
let category = editor.category;
|
||||
for (let item of this._showFor) {
|
||||
if (item.categories.has(category)) {
|
||||
item.elt.classList.remove("plugin-hidden");
|
||||
} else {
|
||||
item.elt.classList.add("plugin-hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onEditorDeactivated: function(editor) {
|
||||
if (this._showFor) {
|
||||
for (let item of this._showFor) {
|
||||
item.elt.classList.add("plugin-hidden");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onEditorLoad: function(editor) {},
|
||||
onEditorSave: function(editor) {},
|
||||
onEditorChange: function(editor) {},
|
||||
onEditorCursorActivity: function(editor) {},
|
||||
});
|
||||
exports.Plugin = Plugin;
|
||||
|
||||
function registerPlugin(constr) {
|
||||
exports.registeredPlugins.push(constr);
|
||||
}
|
||||
exports.registerPlugin = registerPlugin;
|
||||
|
||||
exports.registeredPlugins = [];
|
|
@ -0,0 +1,38 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
const { getLocalizedString } = require("projecteditor/helpers/l10n");
|
||||
|
||||
var DeletePlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function(host) {
|
||||
this.host.addCommand({
|
||||
id: "cmd-delete"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: "#directory-menu-popup",
|
||||
label: getLocalizedString("projecteditor.deleteLabel"),
|
||||
command: "cmd-delete"
|
||||
});
|
||||
},
|
||||
|
||||
onCommand: function(cmd) {
|
||||
if (cmd === "cmd-delete") {
|
||||
let tree = this.host.projectTree;
|
||||
let resource = tree.getSelectedResource();
|
||||
let parent = resource.parent;
|
||||
tree.deleteResource(resource).then(() => {
|
||||
this.host.project.refresh();
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.DeletePlugin = DeletePlugin;
|
||||
registerPlugin(DeletePlugin);
|
|
@ -0,0 +1,43 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
const { emit } = require("sdk/event/core");
|
||||
|
||||
var DirtyPlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
onEditorSave: function(editor) { this.onEditorChange(editor); },
|
||||
onEditorLoad: function(editor) { this.onEditorChange(editor); },
|
||||
|
||||
onEditorChange: function(editor) {
|
||||
// Only run on a TextEditor
|
||||
if (!editor || !editor.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dont' force a refresh unless the dirty state has changed...
|
||||
let priv = this.priv(editor);
|
||||
let clean = editor.editor.isClean();
|
||||
if (priv.isClean !== clean) {
|
||||
|
||||
let resource = editor.shell.resource;
|
||||
emit(resource, "label-change", resource);
|
||||
priv.isClean = clean;
|
||||
}
|
||||
},
|
||||
|
||||
onAnnotate: function(resource, editor, elt) {
|
||||
if (editor && editor.editor && !editor.editor.isClean()) {
|
||||
elt.textContent = '*' + resource.displayName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.DirtyPlugin = DirtyPlugin;
|
||||
|
||||
registerPlugin(DirtyPlugin);
|
|
@ -0,0 +1,41 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { ItchEditor } = require("projecteditor/editors");
|
||||
|
||||
var ImageEditor = Class({
|
||||
extends: ItchEditor,
|
||||
|
||||
initialize: function(document) {
|
||||
ItchEditor.prototype.initialize.apply(this, arguments);
|
||||
this.label = "image";
|
||||
this.appended = promise.resolve();
|
||||
},
|
||||
|
||||
load: function(resource) {
|
||||
let image = this.doc.createElement("image");
|
||||
image.className = "editor-image";
|
||||
image.setAttribute("src", resource.uri);
|
||||
|
||||
let box1 = this.doc.createElement("box");
|
||||
box1.appendChild(image);
|
||||
|
||||
let box2 = this.doc.createElement("box");
|
||||
box2.setAttribute("flex", 1);
|
||||
|
||||
this.elt.appendChild(box1);
|
||||
this.elt.appendChild(box2);
|
||||
|
||||
this.appended.then(() => {
|
||||
this.emit("load");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.ImageEditor = ImageEditor;
|
|
@ -0,0 +1,28 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { ImageEditor } = require("./image-editor");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
|
||||
var ImageEditorPlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
editorForResource: function(node) {
|
||||
if (node.contentCategory === "image") {
|
||||
return ImageEditor;
|
||||
}
|
||||
},
|
||||
|
||||
init: function(host) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
exports.ImageEditorPlugin = ImageEditorPlugin;
|
||||
registerPlugin(ImageEditorPlugin);
|
|
@ -0,0 +1,29 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
var { Class } = require("sdk/core/heritage");
|
||||
var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
|
||||
var LoggingPlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
// Editor state lifetime...
|
||||
onEditorCreated: function(editor) { console.log("editor created: " + editor) },
|
||||
onEditorDestroyed: function(editor) { console.log("editor destroyed: " + editor )},
|
||||
|
||||
onEditorSave: function(editor) { console.log("editor saved: " + editor) },
|
||||
onEditorLoad: function(editor) { console.log("editor loaded: " + editor) },
|
||||
|
||||
onEditorActivated: function(editor) { console.log("editor activated: " + editor )},
|
||||
onEditorDeactivated: function(editor) { console.log("editor deactivated: " + editor )},
|
||||
|
||||
onEditorChange: function(editor) { console.log("editor changed: " + editor )},
|
||||
|
||||
onCommand: function(cmd) { console.log("Command: " + cmd); }
|
||||
});
|
||||
exports.LoggingPlugin = LoggingPlugin;
|
||||
|
||||
registerPlugin(LoggingPlugin);
|
|
@ -0,0 +1,90 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
const { getLocalizedString } = require("projecteditor/helpers/l10n");
|
||||
|
||||
// Handles the new command.
|
||||
var NewFile = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function(host) {
|
||||
this.host.createMenuItem({
|
||||
parent: "#file-menu-popup",
|
||||
label: getLocalizedString("projecteditor.newLabel"),
|
||||
command: "cmd-new",
|
||||
key: "key-new"
|
||||
});
|
||||
this.host.createMenuItem({
|
||||
parent: "#directory-menu-popup",
|
||||
label: getLocalizedString("projecteditor.newLabel"),
|
||||
command: "cmd-new"
|
||||
});
|
||||
|
||||
this.command = this.host.addCommand({
|
||||
id: "cmd-new",
|
||||
key: getLocalizedString("projecteditor.new.commandkey"),
|
||||
modifiers: "accel"
|
||||
});
|
||||
},
|
||||
|
||||
onCommand: function(cmd) {
|
||||
if (cmd === "cmd-new") {
|
||||
let tree = this.host.projectTree;
|
||||
let resource = tree.getSelectedResource();
|
||||
parent = resource.isDir ? resource : resource.parent;
|
||||
sibling = resource.isDir ? null : resource;
|
||||
|
||||
if (!("createChild" in parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let extension = sibling ? sibling.contentCategory : parent.store.defaultCategory;
|
||||
let template = "untitled{1}." + extension;
|
||||
let name = this.suggestName(parent, template);
|
||||
|
||||
tree.promptNew(name, parent, sibling).then(name => {
|
||||
|
||||
// XXX: sanitize bad file names.
|
||||
|
||||
// If the name is already taken, just add/increment a number.
|
||||
if (this.hasChild(parent, name)) {
|
||||
let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
|
||||
template = matches[1] + "{1}" + matches[3] + matches[4];
|
||||
name = this.suggestName(parent, template, parseInt(matches[2]) || 2);
|
||||
}
|
||||
|
||||
return parent.createChild(name);
|
||||
}).then(resource => {
|
||||
tree.selectResource(resource);
|
||||
this.host.currentEditor.focus();
|
||||
}).then(null, console.error);
|
||||
}
|
||||
},
|
||||
|
||||
suggestName: function(parent, template, start=1) {
|
||||
let i = start;
|
||||
let name;
|
||||
do {
|
||||
name = template.replace("\{1\}", i === 1 ? "" : i);
|
||||
i++;
|
||||
} while (this.hasChild(parent, name));
|
||||
|
||||
return name;
|
||||
},
|
||||
|
||||
hasChild: function(resource, name) {
|
||||
for (let child of resource.children) {
|
||||
if (child.basename === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
exports.NewFile = NewFile;
|
||||
registerPlugin(NewFile);
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
const picker = require("projecteditor/helpers/file-picker");
|
||||
const { getLocalizedString } = require("projecteditor/helpers/l10n");
|
||||
|
||||
// Handles the save command.
|
||||
var SavePlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function(host) {
|
||||
|
||||
this.host.addCommand({
|
||||
id: "cmd-saveas",
|
||||
key: getLocalizedString("projecteditor.save.commandkey"),
|
||||
modifiers: "accel shift"
|
||||
});
|
||||
this.host.addCommand({
|
||||
id: "cmd-save",
|
||||
key: getLocalizedString("projecteditor.save.commandkey"),
|
||||
modifiers: "accel"
|
||||
});
|
||||
|
||||
// Wait until we can add things into the app manager menu
|
||||
// this.host.createMenuItem({
|
||||
// parent: "#file-menu-popup",
|
||||
// label: "Save",
|
||||
// command: "cmd-save",
|
||||
// key: "key-save"
|
||||
// });
|
||||
// this.host.createMenuItem({
|
||||
// parent: "#file-menu-popup",
|
||||
// label: "Save As",
|
||||
// command: "cmd-saveas",
|
||||
// });
|
||||
},
|
||||
|
||||
onCommand: function(cmd) {
|
||||
if (cmd === "cmd-save") {
|
||||
this.save();
|
||||
} else if (cmd === "cmd-saveas") {
|
||||
this.saveAs();
|
||||
}
|
||||
},
|
||||
|
||||
saveAs: function() {
|
||||
let editor = this.host.currentEditor;
|
||||
let project = this.host.resourceFor(editor);
|
||||
|
||||
let resource;
|
||||
picker.showSave({
|
||||
window: this.host.window,
|
||||
directory: project && project.parent ? project.parent.path : null,
|
||||
defaultName: project ? project.basename : null,
|
||||
}).then(path => {
|
||||
return this.createResource(path);
|
||||
}).then(res => {
|
||||
resource = res;
|
||||
return this.saveResource(editor, resource);
|
||||
}).then(() => {
|
||||
this.host.openResource(resource);
|
||||
}).then(null, console.error);
|
||||
},
|
||||
|
||||
save: function() {
|
||||
let editor = this.host.currentEditor;
|
||||
let resource = this.host.resourceFor(editor);
|
||||
if (!resource) {
|
||||
return this.saveAs();
|
||||
}
|
||||
|
||||
return this.saveResource(editor, resource);
|
||||
},
|
||||
|
||||
createResource: function(path) {
|
||||
return this.host.project.resourceFor(path, { create: true })
|
||||
},
|
||||
|
||||
saveResource: function(editor, resource) {
|
||||
return editor.save(resource);
|
||||
}
|
||||
})
|
||||
exports.SavePlugin = SavePlugin;
|
||||
registerPlugin(SavePlugin);
|
|
@ -0,0 +1,105 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
|
||||
|
||||
/**
|
||||
* Print information about the currently opened file
|
||||
* and the state of the current editor
|
||||
*/
|
||||
var StatusBarPlugin = Class({
|
||||
extends: Plugin,
|
||||
|
||||
init: function() {
|
||||
this.box = this.host.createElement("hbox", {
|
||||
parent: "#projecteditor-toolbar-bottom"
|
||||
});
|
||||
|
||||
this.activeMode = this.host.createElement("label", {
|
||||
parent: this.box,
|
||||
class: "projecteditor-basic-display"
|
||||
});
|
||||
|
||||
this.cursorPosition = this.host.createElement("label", {
|
||||
parent: this.box,
|
||||
class: "projecteditor-basic-display"
|
||||
});
|
||||
|
||||
this.fileLabel = this.host.createElement("label", {
|
||||
parent: "#plugin-toolbar-left",
|
||||
class: "projecteditor-file-label"
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Print information about the current state of the editor
|
||||
*
|
||||
* @param Editor editor
|
||||
*/
|
||||
render: function(editor, resource) {
|
||||
if (!resource || resource.isDir) {
|
||||
this.fileLabel.textContent = "";
|
||||
this.cursorPosition.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileLabel.textContent = resource.basename;
|
||||
this.activeMode.value = editor.toString();
|
||||
if (editor.editor) {
|
||||
let cursorStart = editor.editor.getCursor("start");
|
||||
let cursorEnd = editor.editor.getCursor("end");
|
||||
if (cursorStart.line === cursorEnd.line && cursorStart.ch === cursorEnd.ch) {
|
||||
this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch;
|
||||
} else {
|
||||
this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch + " | " +
|
||||
cursorEnd.line + " " + cursorEnd.ch;
|
||||
}
|
||||
} else {
|
||||
this.cursorPosition.value = "";
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Print the current file name
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
onTreeSelected: function(resource) {
|
||||
if (!resource || resource.isDir) {
|
||||
this.fileLabel.textContent = "";
|
||||
return;
|
||||
}
|
||||
this.fileLabel.textContent = resource.basename;
|
||||
},
|
||||
|
||||
onEditorDeactivated: function(editor) {
|
||||
this.fileLabel.textContent = "";
|
||||
this.cursorPosition.value = "";
|
||||
},
|
||||
|
||||
onEditorChange: function(editor, resource) {
|
||||
this.render(editor, resource);
|
||||
},
|
||||
|
||||
onEditorCursorActivity: function(editor, resource) {
|
||||
this.render(editor, resource);
|
||||
},
|
||||
|
||||
onEditorActivated: function(editor, resource) {
|
||||
this.render(editor, resource);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
exports.StatusBarPlugin = StatusBarPlugin;
|
||||
registerPlugin(StatusBarPlugin);
|
|
@ -0,0 +1,239 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { scope, on, forget } = require("projecteditor/helpers/event");
|
||||
const prefs = require("sdk/preferences/service");
|
||||
const { LocalStore } = require("projecteditor/stores/local");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { TextEncoder, TextDecoder } = require('sdk/io/buffer');
|
||||
const url = require('sdk/url');
|
||||
|
||||
const gDecoder = new TextDecoder();
|
||||
const gEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* A Project keeps track of the opened folders using LocalStore
|
||||
* objects. Resources are generally requested from the project,
|
||||
* even though the Store is actually keeping track of them.
|
||||
*/
|
||||
var Project = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Intialize the Project.
|
||||
*
|
||||
* @param Object options
|
||||
* Options to be passed into Project.load function
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.localStores = new Map();
|
||||
|
||||
this.load(options);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
// We are removing the store because the project never gets persisted.
|
||||
// There may need to be separate destroy functionality that doesn't remove
|
||||
// from project if this is saved to DB.
|
||||
this.removeAllStores();
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return "[Project] " + this.name;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a project given metadata about it.
|
||||
*
|
||||
* @param Object options
|
||||
* Information about the project, containing:
|
||||
* id: An ID (currently unused, but could be used for saving)
|
||||
* name: The display name of the project
|
||||
* directories: An array of path strings to load
|
||||
*/
|
||||
load: function(options) {
|
||||
this.id = options.id;
|
||||
this.name = options.name || "Untitled";
|
||||
|
||||
let paths = new Set(options.directories.map(name => OS.Path.normalize(name)));
|
||||
|
||||
for (let [path, store] of this.localStores) {
|
||||
if (!paths.has(path)) {
|
||||
this.removePath(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (let path of paths) {
|
||||
this.addPath(path);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh all project stores from disk
|
||||
*
|
||||
* @returns Promise
|
||||
* A promise that resolves when everything has been refreshed.
|
||||
*/
|
||||
refresh: function() {
|
||||
return Task.spawn(function*() {
|
||||
for (let [path, store] of this.localStores) {
|
||||
yield store.refresh();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Fetch a resource from the backing storage system for the store.
|
||||
*
|
||||
* @param string path
|
||||
* The path to fetch
|
||||
* @param Object options
|
||||
* "create": bool indicating whether to create a file if it does not exist.
|
||||
* @returns Promise
|
||||
* A promise that resolves with the Resource.
|
||||
*/
|
||||
resourceFor: function(path, options) {
|
||||
let store = this.storeContaining(path);
|
||||
return store.resourceFor(path, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get every resource used inside of the project.
|
||||
*
|
||||
* @returns Array<Resource>
|
||||
* A list of all Resources in all Stores.
|
||||
*/
|
||||
allResources: function() {
|
||||
let resources = [];
|
||||
for (let store of this.allStores()) {
|
||||
resources = resources.concat(store.allResources());
|
||||
}
|
||||
return resources;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get every Path used inside of the project.
|
||||
*
|
||||
* @returns generator-iterator<Store>
|
||||
* A list of all Stores
|
||||
*/
|
||||
allStores: function*() {
|
||||
for (let [path, store] of this.localStores) {
|
||||
yield store;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get every file path used inside of the project.
|
||||
*
|
||||
* @returns generator-iterator<string>
|
||||
* A list of all file paths
|
||||
*/
|
||||
allPaths: function*() {
|
||||
for (let [path, store] of this.localStores) {
|
||||
yield path;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the store that contains a path.
|
||||
*
|
||||
* @returns Store
|
||||
* The store, if any. Will return null if no store
|
||||
* contains the given path.
|
||||
*/
|
||||
storeContaining: function(path) {
|
||||
let containingStore = null;
|
||||
for (let store of this.allStores()) {
|
||||
if (store.contains(path)) {
|
||||
// With nested projects, the final containing store will be returned.
|
||||
containingStore = store;
|
||||
}
|
||||
}
|
||||
return containingStore;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a store at the current path. If a store already exists
|
||||
* for this path, then return it.
|
||||
*
|
||||
* @param string path
|
||||
* @returns LocalStore
|
||||
*/
|
||||
addPath: function(path) {
|
||||
if (!this.localStores.has(path)) {
|
||||
this.addLocalStore(new LocalStore(path));
|
||||
}
|
||||
return this.localStores.get(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a store for a given path.
|
||||
*
|
||||
* @param string path
|
||||
*/
|
||||
removePath: function(path) {
|
||||
this.removeLocalStore(this.localStores.get(path));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Add the given Store to the project.
|
||||
* Fires a 'store-added' event on the project.
|
||||
*
|
||||
* @param Store store
|
||||
*/
|
||||
addLocalStore: function(store) {
|
||||
store.canPair = true;
|
||||
this.localStores.set(store.path, store);
|
||||
|
||||
// Originally StoreCollection.addStore
|
||||
on(this, store, "resource-added", (resource) => {
|
||||
emit(this, "resource-added", resource);
|
||||
});
|
||||
on(this, store, "resource-removed", (resource) => {
|
||||
emit(this, "resource-removed", resource);
|
||||
})
|
||||
|
||||
emit(this, "store-added", store);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove all of the Stores belonging to the project.
|
||||
*/
|
||||
removeAllStores: function() {
|
||||
for (let store of this.allStores()) {
|
||||
this.removeLocalStore(store);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the given Store from the project.
|
||||
* Fires a 'store-removed' event on the project.
|
||||
*
|
||||
* @param Store store
|
||||
*/
|
||||
removeLocalStore: function(store) {
|
||||
// XXX: tree selection should be reset if active element is affected by
|
||||
// the store being removed
|
||||
if (store) {
|
||||
this.localStores.delete(store.path);
|
||||
forget(this, store);
|
||||
emit(this, "store-removed", store);
|
||||
store.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.Project = Project;
|
|
@ -0,0 +1,594 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { Project } = require("projecteditor/project");
|
||||
const { ProjectTreeView } = require("projecteditor/tree");
|
||||
const { ShellDeck } = require("projecteditor/shells");
|
||||
const { Resource } = require("projecteditor/stores/resource");
|
||||
const { registeredPlugins } = require("projecteditor/plugins/core");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { on, forget } = require("projecteditor/helpers/event");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
|
||||
const { DOMHelpers } = Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
|
||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
|
||||
|
||||
// Enabled Plugins
|
||||
require("projecteditor/plugins/dirty/lib/dirty");
|
||||
require("projecteditor/plugins/delete/lib/delete");
|
||||
require("projecteditor/plugins/new/lib/new");
|
||||
require("projecteditor/plugins/save/lib/save");
|
||||
require("projecteditor/plugins/image-view/lib/plugin");
|
||||
require("projecteditor/plugins/app-manager/lib/plugin");
|
||||
require("projecteditor/plugins/status-bar/lib/plugin");
|
||||
|
||||
// Uncomment to enable logging.
|
||||
// require("projecteditor/plugins/logging/lib/logging");
|
||||
|
||||
/**
|
||||
* This is the main class tying together an instance of the ProjectEditor.
|
||||
* The frontend is contained inside of this.iframe, which loads projecteditor.xul.
|
||||
*
|
||||
* Usage:
|
||||
* let projecteditor = new ProjectEditor(frame);
|
||||
* projecteditor.loaded.then((projecteditor) => {
|
||||
* // Ready to use.
|
||||
* });
|
||||
*
|
||||
* Responsible for maintaining:
|
||||
* - The list of Plugins for this instance.
|
||||
* - The ShellDeck, which includes all Shells for opened Resources
|
||||
* -- Shells take in a Resource, and construct the appropriate Editor
|
||||
* - The Project, which includes all Stores for this instance
|
||||
* -- Stores manage all Resources starting from a root directory
|
||||
* --- Resources are a representation of a file on disk
|
||||
* - The ProjectTreeView that builds the UI for interacting with the
|
||||
* project.
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "onEditorDestroyed": When editor is destroyed
|
||||
* - "onEditorSave": When editor is saved
|
||||
* - "onEditorLoad": When editor is loaded
|
||||
* - "onEditorActivated": When editor is activated
|
||||
* - "onEditorChange": When editor is changed
|
||||
* - "onEditorCursorActivity": When there is cursor activity in a text editor
|
||||
* - "onCommand": When a command happens
|
||||
* - "onEditorDestroyed": When editor is destroyed
|
||||
*
|
||||
* The events can be bound like so:
|
||||
* projecteditor.on("onEditorCreated", (editor) => { });
|
||||
*/
|
||||
var ProjectEditor = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Initialize ProjectEditor, and load into an iframe if specified.
|
||||
*
|
||||
* @param Iframe iframe
|
||||
* The iframe to inject the DOM into. If this is not
|
||||
* specified, then this.load(frame) will need to be called
|
||||
* before accessing ProjectEditor.
|
||||
*/
|
||||
initialize: function(iframe) {
|
||||
this._onTreeSelected = this._onTreeSelected.bind(this);
|
||||
this._onEditorCreated = this._onEditorCreated.bind(this);
|
||||
this._onEditorActivated = this._onEditorActivated.bind(this);
|
||||
this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
|
||||
this._updateEditorMenuItems = this._updateEditorMenuItems.bind(this);
|
||||
|
||||
if (iframe) {
|
||||
this.load(iframe);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the instance inside of a specified iframe.
|
||||
* This can be called more than once, and it will return the promise
|
||||
* from the first call.
|
||||
*
|
||||
* @param Iframe iframe
|
||||
* The iframe to inject the projecteditor DOM into
|
||||
* @returns Promise
|
||||
* A promise that is resolved once the iframe has been
|
||||
* loaded.
|
||||
*/
|
||||
load: function(iframe) {
|
||||
if (this.loaded) {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
let deferred = promise.defer();
|
||||
this.loaded = deferred.promise;
|
||||
this.iframe = iframe;
|
||||
|
||||
let domReady = () => {
|
||||
this._onLoad();
|
||||
deferred.resolve(this);
|
||||
};
|
||||
|
||||
let domHelper = new DOMHelpers(this.iframe.contentWindow);
|
||||
domHelper.onceDOMReady(domReady);
|
||||
|
||||
this.iframe.setAttribute("src", ITCHPAD_URL);
|
||||
|
||||
return this.loaded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the projecteditor DOM inside of this.iframe.
|
||||
*/
|
||||
_onLoad: function() {
|
||||
this.document = this.iframe.contentDocument;
|
||||
this.window = this.iframe.contentWindow;
|
||||
|
||||
this._buildSidebar();
|
||||
|
||||
this.window.addEventListener("unload", this.destroy.bind(this));
|
||||
|
||||
// Editor management
|
||||
this.shells = new ShellDeck(this, this.document);
|
||||
this.shells.on("editor-created", this._onEditorCreated);
|
||||
this.shells.on("editor-activated", this._onEditorActivated);
|
||||
this.shells.on("editor-deactivated", this._onEditorDeactivated);
|
||||
|
||||
let shellContainer = this.document.querySelector("#shells-deck-container");
|
||||
shellContainer.appendChild(this.shells.elt);
|
||||
|
||||
let popup = this.document.querySelector("#edit-menu-popup");
|
||||
popup.addEventListener("popupshowing", this.updateEditorMenuItems);
|
||||
|
||||
// We are not allowing preset projects for now - rebuild a fresh one
|
||||
// each time.
|
||||
this.setProject(new Project({
|
||||
id: "",
|
||||
name: "",
|
||||
directories: [],
|
||||
openFiles: []
|
||||
}));
|
||||
|
||||
this._initCommands();
|
||||
this._initPlugins();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create the project tree sidebar that lists files.
|
||||
*/
|
||||
_buildSidebar: function() {
|
||||
this.projectTree = new ProjectTreeView(this.document, {
|
||||
resourceVisible: this.resourceVisible.bind(this),
|
||||
resourceFormatter: this.resourceFormatter.bind(this)
|
||||
});
|
||||
this.projectTree.on("selection", this._onTreeSelected);
|
||||
|
||||
let sourcesBox = this.document.querySelector("#sources");
|
||||
sourcesBox.appendChild(this.projectTree.elt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up listeners for commands to dispatch to all of the plugins
|
||||
*/
|
||||
_initCommands: function() {
|
||||
this.commands = this.document.querySelector("#projecteditor-commandset");
|
||||
this.commands.addEventListener("command", (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
this.pluginDispatch("onCommand", evt.target.id, evt.target);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize each plugin in registeredPlugins
|
||||
*/
|
||||
_initPlugins: function() {
|
||||
this._plugins = [];
|
||||
|
||||
for (let plugin of registeredPlugins) {
|
||||
try {
|
||||
this._plugins.push(plugin(this));
|
||||
} catch(ex) {
|
||||
console.exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
this.pluginDispatch("lateInit");
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable / disable necessary menu items using globalOverlay.js.
|
||||
*/
|
||||
_updateEditorMenuItems: function() {
|
||||
this.window.goUpdateGlobalEditMenuItems();
|
||||
this.window.goUpdateGlobalEditMenuItems();
|
||||
let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
|
||||
commands.forEach(this.window.goUpdateCommand);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy all objects on the iframe unload event.
|
||||
*/
|
||||
destroy: function() {
|
||||
this._plugins.forEach(plugin => { plugin.destroy(); });
|
||||
|
||||
this.project.allResources().forEach((resource) => {
|
||||
let editor = this.editorFor(resource);
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
forget(this, this.project);
|
||||
this.project.destroy();
|
||||
this.project = null;
|
||||
this.projectTree.destroy();
|
||||
this.projectTree = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the current project viewed by the projecteditor.
|
||||
*
|
||||
* @param Project project
|
||||
* The project to set.
|
||||
*/
|
||||
setProject: function(project) {
|
||||
if (this.project) {
|
||||
forget(this, this.project);
|
||||
}
|
||||
this.project = project;
|
||||
this.projectTree.setProject(project);
|
||||
|
||||
// Whenever a store gets removed, clean up any editors that
|
||||
// exist for resources within it.
|
||||
on(this, project, "store-removed", (store) => {
|
||||
store.allResources().forEach((resource) => {
|
||||
let editor = this.editorFor(resource);
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the current project viewed by the projecteditor to a single path,
|
||||
* used by the app manager.
|
||||
*
|
||||
* @param string path
|
||||
* The file path to set
|
||||
* @param Object opts
|
||||
* Custom options used by the project. See plugins/app-manager.
|
||||
* @param Promise
|
||||
* Promise that is resolved once the project is ready to be used.
|
||||
*/
|
||||
setProjectToAppPath: function(path, opts = {}) {
|
||||
this.project.appManagerOpts = opts;
|
||||
this.project.removeAllStores();
|
||||
this.project.addPath(path);
|
||||
return this.project.refresh();
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a resource in a particular shell.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to be opened.
|
||||
*/
|
||||
openResource: function(resource) {
|
||||
this.shells.open(resource);
|
||||
this.projectTree.selectResource(resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* When a node is selected in the tree, open its associated editor.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file that has been selected
|
||||
*/
|
||||
_onTreeSelected: function(resource) {
|
||||
// Don't attempt to open a directory that is not the root element.
|
||||
if (resource.isDir && resource.parent) {
|
||||
return;
|
||||
}
|
||||
this.pluginDispatch("onTreeSelected", resource);
|
||||
this.openResource(resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an xul element with options
|
||||
*
|
||||
* @param string type
|
||||
* The tag name of the element to create.
|
||||
* @param Object options
|
||||
* "command": DOMNode or string ID of a command element.
|
||||
* "parent": DOMNode or selector of parent to append child to.
|
||||
* anything other keys are set as an attribute as the element.
|
||||
* @returns DOMElement
|
||||
* The element that has been created.
|
||||
*/
|
||||
createElement: function(type, options) {
|
||||
let elt = this.document.createElement(type);
|
||||
|
||||
let parent;
|
||||
|
||||
for (let opt in options) {
|
||||
if (opt === "command") {
|
||||
let command = typeof(options.command) === "string" ? options.command : options.command.id;
|
||||
elt.setAttribute("command", command);
|
||||
} else if (opt === "parent") {
|
||||
continue;
|
||||
} else {
|
||||
elt.setAttribute(opt, options[opt]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.parent) {
|
||||
let parent = options.parent;
|
||||
if (typeof(parent) === "string") {
|
||||
parent = this.document.querySelector(parent);
|
||||
}
|
||||
parent.appendChild(elt);
|
||||
}
|
||||
|
||||
return elt;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a "menuitem" xul element with options
|
||||
*
|
||||
* @param Object options
|
||||
* See createElement for available options.
|
||||
* @returns DOMElement
|
||||
* The menuitem that has been created.
|
||||
*/
|
||||
createMenuItem: function(options) {
|
||||
return this.createElement("menuitem", options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a command to the projecteditor document.
|
||||
* This method is meant to be used with plugins.
|
||||
*
|
||||
* @param Object definition
|
||||
* key: a key/keycode string. Example: "f".
|
||||
* id: Unique ID. Example: "find".
|
||||
* modifiers: Key modifiers. Example: "accel".
|
||||
* @returns DOMElement
|
||||
* The command element that has been created.
|
||||
*/
|
||||
addCommand: function(definition) {
|
||||
let command = this.document.createElement("command");
|
||||
command.setAttribute("id", definition.id);
|
||||
if (definition.key) {
|
||||
let key = this.document.createElement("key");
|
||||
key.id = "key_" + definition.id;
|
||||
|
||||
let keyName = definition.key;
|
||||
if (keyName.startsWith("VK_")) {
|
||||
key.setAttribute("keycode", keyName);
|
||||
} else {
|
||||
key.setAttribute("key", keyName);
|
||||
}
|
||||
key.setAttribute("modifiers", definition.modifiers);
|
||||
key.setAttribute("command", definition.id);
|
||||
this.document.getElementById("projecteditor-keyset").appendChild(key);
|
||||
}
|
||||
command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
|
||||
this.document.getElementById("projecteditor-commandset").appendChild(command);
|
||||
return command;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the instance of a plugin registered with a certain type.
|
||||
*
|
||||
* @param Type pluginType
|
||||
* The type, such as SavePlugin
|
||||
* @returns Plugin
|
||||
* The plugin instance matching the specified type.
|
||||
*/
|
||||
getPlugin: function(pluginType) {
|
||||
for (let plugin of this.plugins) {
|
||||
if (plugin.constructor === pluginType) {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all plugin instances active for the current project
|
||||
*
|
||||
* @returns [Plugin]
|
||||
*/
|
||||
get plugins() {
|
||||
if (!this._plugins) {
|
||||
console.log("plugins requested before _plugins was set");
|
||||
return [];
|
||||
}
|
||||
// Could filter further based on the type of project selected,
|
||||
// but no need right now.
|
||||
return this._plugins;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch an onEditorCreated event, and listen for other events specific
|
||||
* to this editor instance.
|
||||
*
|
||||
* @param Editor editor
|
||||
* The new editor instance.
|
||||
*/
|
||||
_onEditorCreated: function(editor) {
|
||||
this.pluginDispatch("onEditorCreated", editor);
|
||||
this._editorListenAndDispatch(editor, "change", "onEditorChange");
|
||||
this._editorListenAndDispatch(editor, "cursorActivity", "onEditorCursorActivity");
|
||||
this._editorListenAndDispatch(editor, "load", "onEditorLoad");
|
||||
this._editorListenAndDispatch(editor, "save", "onEditorSave");
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch an onEditorActivated event and finish setting up once the
|
||||
* editor is ready to use.
|
||||
*
|
||||
* @param Editor editor
|
||||
* The editor instance, which is now appended in the document.
|
||||
* @param Resource resource
|
||||
* The resource used by the editor
|
||||
*/
|
||||
_onEditorActivated: function(editor, resource) {
|
||||
editor.setToolbarVisibility();
|
||||
this.pluginDispatch("onEditorActivated", editor, resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch an onEditorDactivated event once an editor loses focus
|
||||
*
|
||||
* @param Editor editor
|
||||
* The editor instance, which is no longer active.
|
||||
* @param Resource resource
|
||||
* The resource used by the editor
|
||||
*/
|
||||
_onEditorDeactivated: function(editor, resource) {
|
||||
this.pluginDispatch("onEditorDeactivated", editor, resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call a method on all plugins that implement the method.
|
||||
* Also emits the same handler name on `this`.
|
||||
*
|
||||
* @param string handler
|
||||
* Which function name to call on plugins.
|
||||
* @param ...args args
|
||||
* All remaining parameters are passed into the handler.
|
||||
*/
|
||||
pluginDispatch: function(handler, ...args) {
|
||||
// XXX: Memory leak when console.log an Editor here
|
||||
// console.log("DISPATCHING EVENT TO PLUGIN", handler, args);
|
||||
emit(this, handler, ...args);
|
||||
this.plugins.forEach(plugin => {
|
||||
try {
|
||||
if (handler in plugin) plugin[handler](...args);
|
||||
} catch(ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen to an event on the editor object and dispatch it
|
||||
* to all plugins that implement the associated method
|
||||
*
|
||||
* @param Editor editor
|
||||
* Which editor to listen to
|
||||
* @param string event
|
||||
* Which editor event to listen for
|
||||
* @param string handler
|
||||
* Which plugin method to call
|
||||
*/
|
||||
_editorListenAndDispatch: function(editor, event, handler) {
|
||||
/// XXX: Uncommenting this line also causes memory leak.
|
||||
// console.log("Binding listen and dispatch", editor);
|
||||
editor.on(event, (...args) => {
|
||||
this.pluginDispatch(handler, editor, this.resourceFor(editor), ...args);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a shell for a resource.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to be opened.
|
||||
* @returns Shell
|
||||
*/
|
||||
shellFor: function(resource) {
|
||||
return this.shells.shellFor(resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the Editor for a given resource.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to check.
|
||||
* @returns Editor
|
||||
* Instance of the editor for this file.
|
||||
*/
|
||||
editorFor: function(resource) {
|
||||
let shell = this.shellFor(resource);
|
||||
return shell ? shell.editor : shell;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a resource for the given editor
|
||||
*
|
||||
* @param Editor editor
|
||||
* The editor to check
|
||||
* @returns Resource
|
||||
* The resource associated with this editor
|
||||
*/
|
||||
resourceFor: function(editor) {
|
||||
if (editor && editor.shell && editor.shell.resource) {
|
||||
return editor.shell.resource;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decide whether a given resource should be hidden in the tree.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The resource in the tree
|
||||
* @returns Boolean
|
||||
* True if the node should be visible, false if hidden.
|
||||
*/
|
||||
resourceVisible: function(resource) {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format the given node for display in the resource tree view.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to be opened.
|
||||
* @param DOMNode elt
|
||||
* The element in the tree to render into.
|
||||
*/
|
||||
resourceFormatter: function(resource, elt) {
|
||||
let editor = this.editorFor(resource);
|
||||
let renderedByPlugin = false;
|
||||
|
||||
// Allow plugins to override default templating of resource in tree.
|
||||
this.plugins.forEach(plugin => {
|
||||
if (!plugin.onAnnotate) {
|
||||
return;
|
||||
}
|
||||
if (plugin.onAnnotate(resource, editor, elt)) {
|
||||
renderedByPlugin = true;
|
||||
}
|
||||
});
|
||||
|
||||
// If no plugin wants to handle it, just use a string from the resource.
|
||||
if (!renderedByPlugin) {
|
||||
elt.textContent = resource.displayName;
|
||||
}
|
||||
},
|
||||
|
||||
get sourcesVisible() {
|
||||
return this.sourceToggle.hasAttribute("pane-collapsed");
|
||||
},
|
||||
|
||||
get currentShell() {
|
||||
return this.shells.currentShell;
|
||||
},
|
||||
|
||||
get currentEditor() {
|
||||
return this.shells.currentEditor;
|
||||
},
|
||||
});
|
||||
|
||||
exports.ProjectEditor = ProjectEditor;
|
|
@ -0,0 +1,210 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { EditorTypeForResource } = require("projecteditor/editors");
|
||||
const NetworkHelper = require("devtools/toolkit/webconsole/network-helper");
|
||||
|
||||
/**
|
||||
* The Shell is the object that manages the editor for a single resource.
|
||||
* It is in charge of selecting the proper Editor (text/image/plugin-defined)
|
||||
* and instantiating / appending the editor.
|
||||
* This object is not exported, it is just used internally by the ShellDeck.
|
||||
*
|
||||
* This object has a promise `editorAppended`, that will resolve once the editor
|
||||
* is ready to be used.
|
||||
*/
|
||||
var Shell = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* @param ProjectEditor host
|
||||
* @param Resource resource
|
||||
*/
|
||||
initialize: function(host, resource) {
|
||||
this.host = host;
|
||||
this.doc = host.document;
|
||||
this.resource = resource;
|
||||
this.elt = this.doc.createElement("vbox");
|
||||
this.elt.shell = this;
|
||||
|
||||
let constructor = this._editorTypeForResource();
|
||||
|
||||
this.editor = constructor(this.doc, this.host);
|
||||
this.editor.shell = this;
|
||||
this.editorAppended = this.editor.appended;
|
||||
|
||||
let loadDefer = promise.defer();
|
||||
this.editor.on("load", () => {
|
||||
loadDefer.resolve();
|
||||
});
|
||||
|
||||
this.editorLoaded = loadDefer.promise;
|
||||
|
||||
this.elt.appendChild(this.editor.elt);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start loading the resource. The 'load' event happens as
|
||||
* a result of this function, so any listeners to 'editorAppended'
|
||||
* need to be added before calling this.
|
||||
*/
|
||||
load: function() {
|
||||
this.editor.load(this.resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make sure the correct editor is selected for the resource.
|
||||
* @returns Type:Editor
|
||||
*/
|
||||
_editorTypeForResource: function() {
|
||||
let resource = this.resource;
|
||||
let constructor = EditorTypeForResource(resource);
|
||||
|
||||
if (this.host.plugins) {
|
||||
this.host.plugins.forEach(plugin => {
|
||||
if (plugin.editorForResource) {
|
||||
let pluginEditor = plugin.editorForResource(resource);
|
||||
if (pluginEditor) {
|
||||
constructor = pluginEditor;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return constructor;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The ShellDeck is in charge of managing the list of active Shells for
|
||||
* the current ProjectEditor instance (aka host).
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "editor-created": When an editor is initially created
|
||||
* - "editor-activated": When an editor is ready to use
|
||||
* - "editor-deactivated": When an editor is ready to use
|
||||
*/
|
||||
var ShellDeck = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* @param ProjectEditor host
|
||||
* @param Document document
|
||||
*/
|
||||
initialize: function(host, document) {
|
||||
this.doc = document;
|
||||
this.host = host;
|
||||
this.deck = this.doc.createElement("deck");
|
||||
this.deck.setAttribute("flex", "1");
|
||||
this.elt = this.deck;
|
||||
|
||||
this.shells = new Map();
|
||||
|
||||
this._activeShell = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a resource in a Shell. Will create the Shell
|
||||
* if it doesn't exist yet.
|
||||
*
|
||||
* @param Resource resource
|
||||
* The file to be opened
|
||||
* @returns Shell
|
||||
*/
|
||||
open: function(defaultResource) {
|
||||
let shell = this.shellFor(defaultResource);
|
||||
if (!shell) {
|
||||
shell = this._createShell(defaultResource);
|
||||
this.shells.set(defaultResource, shell);
|
||||
}
|
||||
this.selectShell(shell);
|
||||
return shell;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new Shell for a resource. Called by `open`.
|
||||
*
|
||||
* @returns Shell
|
||||
*/
|
||||
_createShell: function(defaultResource) {
|
||||
let shell = Shell(this.host, defaultResource);
|
||||
|
||||
shell.editorAppended.then(() => {
|
||||
this.shells.set(shell.resource, shell);
|
||||
emit(this, "editor-created", shell.editor);
|
||||
if (this.currentShell === shell) {
|
||||
this.selectShell(shell);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
shell.load();
|
||||
this.deck.appendChild(shell.elt);
|
||||
return shell;
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a given shell and open its editor.
|
||||
* Will fire editor-deactivated on the old selected Shell (if any),
|
||||
* and editor-activated on the new one once it is ready
|
||||
*
|
||||
* @param Shell shell
|
||||
*/
|
||||
selectShell: function(shell) {
|
||||
// Don't fire another activate if this is already the active shell
|
||||
if (this._activeShell != shell) {
|
||||
if (this._activeShell) {
|
||||
emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
|
||||
}
|
||||
this.deck.selectedPanel = shell.elt;
|
||||
this._activeShell = shell;
|
||||
shell.editorLoaded.then(() => {
|
||||
// Handle case where another shell has been requested before this
|
||||
// one is finished loading.
|
||||
if (this._activeShell === shell) {
|
||||
emit(this, "editor-activated", shell.editor, shell.resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a Shell for a Resource.
|
||||
*
|
||||
* @param Resource resource
|
||||
* @returns Shell
|
||||
*/
|
||||
shellFor: function(resource) {
|
||||
return this.shells.get(resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* The currently active Shell. Note: the editor may not yet be available
|
||||
* on the current shell. Best to wait for the 'editor-activated' event
|
||||
* instead.
|
||||
*
|
||||
* @returns Shell
|
||||
*/
|
||||
get currentShell() {
|
||||
return this._activeShell;
|
||||
},
|
||||
|
||||
/**
|
||||
* The currently active Editor, or null if it is not ready.
|
||||
*
|
||||
* @returns Editor
|
||||
*/
|
||||
get currentEditor() {
|
||||
let shell = this.currentShell;
|
||||
return shell ? shell.editor : null;
|
||||
},
|
||||
|
||||
});
|
||||
exports.ShellDeck = ShellDeck;
|
|
@ -0,0 +1,58 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
|
||||
/**
|
||||
* A Store object maintains a collection of Resource objects stored in a tree.
|
||||
*
|
||||
* The Store class should not be instantiated directly. Instead, you should
|
||||
* use a class extending it - right now this is only a LocalStore.
|
||||
*
|
||||
* Events:
|
||||
* This object emits the 'resource-added' and 'resource-removed' events.
|
||||
*/
|
||||
var Store = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Should be called during initialize() of a subclass.
|
||||
*/
|
||||
initStore: function() {
|
||||
this.resources = new Map();
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a sorted Array of all Resources in the Store
|
||||
*/
|
||||
allResources: function() {
|
||||
var resources = [];
|
||||
function addResource(resource) {
|
||||
resources.push(resource);
|
||||
resource.childrenSorted.forEach(addResource);
|
||||
}
|
||||
addResource(this.root);
|
||||
return resources;
|
||||
},
|
||||
|
||||
notifyAdd: function(resource) {
|
||||
emit(this, "resource-added", resource);
|
||||
},
|
||||
|
||||
notifyRemove: function(resource) {
|
||||
emit(this, "resource-removed", resource);
|
||||
}
|
||||
});
|
||||
|
||||
exports.Store = Store;
|
|
@ -0,0 +1,219 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { Store } = require("projecteditor/stores/base");
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { on, forget } = require("projecteditor/helpers/event");
|
||||
const { FileResource } = require("projecteditor/stores/resource");
|
||||
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const CHECK_LINKED_DIRECTORY_DELAY = 5000;
|
||||
const SHOULD_LIVE_REFRESH = true;
|
||||
// XXX: Ignores should be customizable
|
||||
const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/;
|
||||
|
||||
/**
|
||||
* A LocalStore object maintains a collection of Resource objects
|
||||
* from the file system.
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "resource-added": When a resource is added
|
||||
* - "resource-removed": When a resource is removed
|
||||
*/
|
||||
var LocalStore = Class({
|
||||
extends: Store,
|
||||
|
||||
defaultCategory: "js",
|
||||
|
||||
initialize: function(path) {
|
||||
this.initStore();
|
||||
this.window = Services.appShell.hiddenDOMWindow;
|
||||
this.path = OS.Path.normalize(path);
|
||||
this.rootPath = this.path;
|
||||
this.displayName = this.path;
|
||||
this.root = this._forPath(this.path);
|
||||
this.notifyAdd(this.root);
|
||||
this.refreshLoop = this.refreshLoop.bind(this);
|
||||
this.refreshLoop();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.window) {
|
||||
this.window.clearTimeout(this._refreshTimeout);
|
||||
}
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.reject("destroy");
|
||||
}
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
this._refreshTimeout = null;
|
||||
this._refreshDeferred = null;
|
||||
this.window = null;
|
||||
this.worker = null;
|
||||
|
||||
if (this.root) {
|
||||
forget(this, this.root);
|
||||
this.root.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
toString: function() { return "[LocalStore:" + this.path + "]" },
|
||||
|
||||
/**
|
||||
* Return a FileResource object for the given path. If a FileInfo
|
||||
* is provided the resource will use it, otherwise the FileResource
|
||||
* might not have full information until the next refresh.
|
||||
*
|
||||
* The following parameters are passed into the FileResource constructor
|
||||
* See resource.js for information about them
|
||||
*
|
||||
* @param String path
|
||||
* @param FileInfo info
|
||||
* @returns Resource
|
||||
*/
|
||||
_forPath: function(path, info=null) {
|
||||
if (this.resources.has(path)) {
|
||||
return this.resources.get(path);
|
||||
}
|
||||
|
||||
let resource = FileResource(this, path, info);
|
||||
this.resources.set(path, resource);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a promise that resolves to a fully-functional FileResource
|
||||
* within this project. This will hit the disk for stat info.
|
||||
* options:
|
||||
*
|
||||
* create: If true, a resource will be created even if the underlying
|
||||
* file doesn't exist.
|
||||
*/
|
||||
resourceFor: function(path, options) {
|
||||
path = OS.Path.normalize(path);
|
||||
|
||||
if (this.resources.has(path)) {
|
||||
return promise.resolve(this.resources.get(path));
|
||||
}
|
||||
|
||||
if (!this.contains(path)) {
|
||||
return promise.reject(new Error(path + " does not belong to " + this.path));
|
||||
}
|
||||
|
||||
return Task.spawn(function() {
|
||||
let parent = yield this.resourceFor(OS.Path.dirname(path));
|
||||
|
||||
let info;
|
||||
try {
|
||||
info = yield OS.File.stat(path);
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
if (!options.create) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
let resource = this._forPath(path, info);
|
||||
parent.addChild(resource);
|
||||
throw new Task.Result(resource);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
refreshLoop: function() {
|
||||
// XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
|
||||
this.refresh().then(() => {
|
||||
if (SHOULD_LIVE_REFRESH) {
|
||||
this._refreshTimeout = this.window.setTimeout(this.refreshLoop,
|
||||
CHECK_LINKED_DIRECTORY_DELAY);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_refreshTimeout: null,
|
||||
_refreshDeferred: null,
|
||||
|
||||
/**
|
||||
* Refresh the directory structure.
|
||||
*/
|
||||
refresh: function(path=this.rootPath) {
|
||||
if (this._refreshDeferred) {
|
||||
return this._refreshDeferred.promise;
|
||||
}
|
||||
this._refreshDeferred = promise.defer();
|
||||
|
||||
let worker = this.worker = new ChromeWorker("chrome://browser/content/devtools/readdir.js");
|
||||
let start = Date.now();
|
||||
|
||||
worker.onmessage = evt => {
|
||||
// console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt);
|
||||
for (path in evt.data) {
|
||||
let info = evt.data[path];
|
||||
info.path = path;
|
||||
|
||||
let resource = this._forPath(path, info);
|
||||
resource.info = info;
|
||||
if (info.isDir) {
|
||||
let newChildren = new Set();
|
||||
for (let childPath of info.children) {
|
||||
childInfo = evt.data[childPath];
|
||||
newChildren.add(this._forPath(childPath, childInfo));
|
||||
}
|
||||
resource.setChildren(newChildren);
|
||||
}
|
||||
resource.info.children = null;
|
||||
}
|
||||
|
||||
worker = null;
|
||||
this._refreshDeferred.resolve();
|
||||
this._refreshDeferred = null;
|
||||
};
|
||||
worker.onerror = ex => {
|
||||
console.error(ex);
|
||||
worker = null;
|
||||
this._refreshDeferred.reject(ex);
|
||||
this._refreshDeferred = null;
|
||||
}
|
||||
worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX });
|
||||
return this._refreshDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the given path would be a child of the store's
|
||||
* root directory.
|
||||
*/
|
||||
contains: function(path) {
|
||||
path = OS.Path.normalize(path);
|
||||
let thisPath = OS.Path.split(this.rootPath);
|
||||
let thatPath = OS.Path.split(path)
|
||||
|
||||
if (!(thisPath.absolute && thatPath.absolute)) {
|
||||
throw new Error("Contains only works with absolute paths.");
|
||||
}
|
||||
|
||||
if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (thatPath.components.length <= thisPath.components.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < thisPath.components.length; i++) {
|
||||
if (thisPath.components[i] != thatPath.components[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
exports.LocalStore = LocalStore;
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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";
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { TextEncoder, TextDecoder } = require('sdk/io/buffer');
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const URL = require("sdk/url");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
|
||||
const gDecoder = new TextDecoder();
|
||||
const gEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* A Resource is a single file-like object that can be respresented
|
||||
* as a file for ProjectEditor.
|
||||
*
|
||||
* The Resource class is not exported, and should not be instantiated
|
||||
* Instead, you should use the FileResource class that extends it.
|
||||
*
|
||||
* This object emits the following events:
|
||||
* - "children-changed": When a child has been added or removed.
|
||||
* See setChildren.
|
||||
*/
|
||||
var Resource = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
refresh: function() { return promise.resolve(this) },
|
||||
|
||||
setURI: function(uri) {
|
||||
if (typeof(uri) === "string") {
|
||||
uri = URL.URL(uri);
|
||||
}
|
||||
this.uri = uri;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the trailing name component of this.uri.
|
||||
*/
|
||||
get basename() { return this.uri.path.replace(/\/+$/, '').replace(/\\/g,'/').replace( /.*\//, '' ); },
|
||||
|
||||
/**
|
||||
* Is there more than 1 child Resource?
|
||||
*/
|
||||
get hasChildren() { return this.children && this.children.size > 0; },
|
||||
|
||||
/**
|
||||
* Sorted array of children for display
|
||||
*/
|
||||
get childrenSorted() {
|
||||
if (!this.hasChildren) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...this.children].sort((a, b)=> {
|
||||
// Put directories above files.
|
||||
if (a.isDir !== b.isDir) {
|
||||
return b.isDir;
|
||||
}
|
||||
return a.basename.toLowerCase() > b.basename.toLowerCase();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the children set of this Resource, and notify of any
|
||||
* additions / removals that happened in the change.
|
||||
*/
|
||||
setChildren: function(newChildren) {
|
||||
let oldChildren = this.children || new Set();
|
||||
let change = false;
|
||||
|
||||
for (let child of oldChildren) {
|
||||
if (!newChildren.has(child)) {
|
||||
change = true;
|
||||
child.parent = null;
|
||||
this.store.notifyRemove(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (let child of newChildren) {
|
||||
if (!oldChildren.has(child)) {
|
||||
change = true;
|
||||
child.parent = this;
|
||||
this.store.notifyAdd(child);
|
||||
}
|
||||
}
|
||||
|
||||
this.children = newChildren;
|
||||
if (change) {
|
||||
emit(this, "children-changed", this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a resource to children set and notify of the change.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
addChild: function(resource) {
|
||||
this.children = this.children || new Set();
|
||||
|
||||
resource.parent = this;
|
||||
this.children.add(resource);
|
||||
this.store.notifyAdd(resource);
|
||||
emit(this, "children-changed", this);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a resource to children set and notify of the change.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
removeChild: function(resource) {
|
||||
resource.parent = null;
|
||||
this.children.remove(resource);
|
||||
this.store.notifyRemove(resource);
|
||||
emit(this, "children-changed", this);
|
||||
return resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a set with children, children of children, etc -
|
||||
* gathered recursively.
|
||||
*
|
||||
* @returns Set<Resource>
|
||||
*/
|
||||
allDescendants: function() {
|
||||
let set = new Set();
|
||||
|
||||
function addChildren(item) {
|
||||
if (!item.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let child of item.children) {
|
||||
set.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
addChildren(this);
|
||||
for (let item of set) {
|
||||
addChildren(item);
|
||||
}
|
||||
|
||||
return set;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A FileResource is an implementation of Resource for a File System
|
||||
* backing. This is exported, and should be used instead of Resource.
|
||||
*/
|
||||
var FileResource = Class({
|
||||
extends: Resource,
|
||||
|
||||
/**
|
||||
* @param Store store
|
||||
* @param String path
|
||||
* @param FileInfo info
|
||||
* https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info
|
||||
*/
|
||||
initialize: function(store, path, info) {
|
||||
this.store = store;
|
||||
this.path = path;
|
||||
|
||||
this.setURI(URL.URL(URL.fromFilename(path)));
|
||||
this._lastReadModification = undefined;
|
||||
|
||||
this.info = info;
|
||||
this.parent = null;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return "[FileResource:" + this.path + "]";
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.reject();
|
||||
}
|
||||
this._refreshDeferred = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch and cache information about this particular file.
|
||||
* https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File_for_the_main_thread#OS.File.stat
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves once the File.stat has finished.
|
||||
*/
|
||||
refresh: function() {
|
||||
if (this._refreshDeferred) {
|
||||
return this._refreshDeferred.promise;
|
||||
}
|
||||
this._refreshDeferred = promise.defer();
|
||||
OS.File.stat(this.path).then(info => {
|
||||
this.info = info;
|
||||
if (this._refreshDeferred) {
|
||||
this._refreshDeferred.resolve(this);
|
||||
this._refreshDeferred = null;
|
||||
}
|
||||
});
|
||||
return this._refreshDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* A string to be used when displaying this Resource in views
|
||||
*/
|
||||
get displayName() {
|
||||
return this.basename + (this.isDir ? "/" : "")
|
||||
},
|
||||
|
||||
/**
|
||||
* Is this FileResource a directory? Rather than checking children
|
||||
* here, we use this.info. So this could return a false negative
|
||||
* if there was no info passed in on constructor and the first
|
||||
* refresh hasn't yet finished.
|
||||
*/
|
||||
get isDir() {
|
||||
if (!this.info) { return false; }
|
||||
return this.info.isDir && !this.info.isSymLink;
|
||||
},
|
||||
|
||||
/**
|
||||
* Read the file as a string asynchronously.
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves with the text of the file.
|
||||
*/
|
||||
load: function() {
|
||||
return OS.File.read(this.path).then(bytes => {
|
||||
return gDecoder.decode(bytes);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a text file as a child of this FileResource.
|
||||
* This instance must be a directory.
|
||||
*
|
||||
* @param string name
|
||||
* The filename (path will be generated based on this.path).
|
||||
* string initial
|
||||
* The content to write to the new file.
|
||||
* @returns Promise
|
||||
* Resolves with the new FileResource once it has
|
||||
* been written to disk.
|
||||
* Rejected if this is not a directory.
|
||||
*/
|
||||
createChild: function(name, initial="") {
|
||||
if (!this.isDir) {
|
||||
return promise.reject(new Error("Cannot add child to a regular file"));
|
||||
}
|
||||
|
||||
let newPath = OS.Path.join(this.path, name);
|
||||
|
||||
let buffer = initial ? gEncoder.encode(initial) : "";
|
||||
return OS.File.writeAtomic(newPath, buffer, {
|
||||
noOverwrite: true
|
||||
}).then(() => {
|
||||
return this.store.refresh();
|
||||
}).then(() => {
|
||||
let resource = this.store.resources.get(newPath);
|
||||
if (!resource) {
|
||||
throw new Error("Error creating " + newPath);
|
||||
}
|
||||
return resource;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Write a string to this file.
|
||||
*
|
||||
* @param string content
|
||||
* @returns Promise
|
||||
* Resolves once it has been written to disk.
|
||||
* Rejected if there is an error
|
||||
*/
|
||||
save: function(content) {
|
||||
let buffer = gEncoder.encode(content);
|
||||
let path = this.path;
|
||||
|
||||
// XXX: writeAtomic was losing permissions after saving on OSX
|
||||
// return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
|
||||
|
||||
return Task.spawn(function*() {
|
||||
let pfh = yield OS.File.open(path, {truncate: true});
|
||||
yield pfh.write(buffer);
|
||||
yield pfh.close();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempts to get the content type from the file.
|
||||
*/
|
||||
get contentType() {
|
||||
if (this._contentType) {
|
||||
return this._contentType;
|
||||
}
|
||||
if (this.isDir) {
|
||||
return "x-directory/normal";
|
||||
}
|
||||
try {
|
||||
this._contentType = mimeService.getTypeFromFile(new FileUtils.File(this.path));
|
||||
} catch(ex) {
|
||||
if (ex.name !== "NS_ERROR_NOT_AVAILABLE" &&
|
||||
ex.name !== "NS_ERROR_FAILURE") {
|
||||
console.error(ex, this.path);
|
||||
}
|
||||
this._contentType = null;
|
||||
}
|
||||
return this._contentType;
|
||||
},
|
||||
|
||||
/**
|
||||
* A string used when determining the type of Editor to open for this.
|
||||
* See editors.js -> EditorTypeForResource.
|
||||
*/
|
||||
get contentCategory() {
|
||||
const NetworkHelper = require("devtools/toolkit/webconsole/network-helper");
|
||||
let category = NetworkHelper.mimeCategoryMap[this.contentType];
|
||||
// Special treatment for manifest.webapp.
|
||||
if (!category && this.basename === "manifest.webapp") {
|
||||
return "json";
|
||||
}
|
||||
return category || "txt";
|
||||
}
|
||||
});
|
||||
|
||||
exports.FileResource = FileResource;
|
|
@ -0,0 +1,557 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const { EventTarget } = require("sdk/event/target");
|
||||
const { merge } = require("sdk/util/object");
|
||||
const promise = require("projecteditor/helpers/promise");
|
||||
const { InplaceEditor } = require("devtools/shared/inplace-editor");
|
||||
const { on, forget } = require("projecteditor/helpers/event");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* ResourceContainer is used as the view of a single Resource in
|
||||
* the tree. It is not exported.
|
||||
*/
|
||||
var ResourceContainer = Class({
|
||||
/**
|
||||
* @param ProjectTreeView tree
|
||||
* @param Resource resource
|
||||
*/
|
||||
initialize: function(tree, resource) {
|
||||
this.tree = tree;
|
||||
this.resource = resource;
|
||||
this.elt = null;
|
||||
this.expander = null;
|
||||
this.children = null;
|
||||
|
||||
let doc = tree.doc;
|
||||
|
||||
this.elt = doc.createElementNS(HTML_NS, "li");
|
||||
this.elt.classList.add("child");
|
||||
|
||||
this.line = doc.createElementNS(HTML_NS, "div");
|
||||
this.line.classList.add("child");
|
||||
this.line.classList.add("side-menu-widget-item");
|
||||
this.line.setAttribute("theme", "dark");
|
||||
this.line.setAttribute("tabindex", "0");
|
||||
|
||||
this.elt.appendChild(this.line);
|
||||
|
||||
this.highlighter = doc.createElementNS(HTML_NS, "span");
|
||||
this.highlighter.classList.add("highlighter");
|
||||
this.line.appendChild(this.highlighter);
|
||||
|
||||
this.expander = doc.createElementNS(HTML_NS, "span");
|
||||
this.expander.className = "arrow expander";
|
||||
this.expander.setAttribute("open", "");
|
||||
this.line.appendChild(this.expander);
|
||||
|
||||
this.icon = doc.createElementNS(HTML_NS, "span");
|
||||
this.line.appendChild(this.icon);
|
||||
|
||||
this.label = doc.createElementNS(HTML_NS, "span");
|
||||
this.label.className = "file-label";
|
||||
this.line.appendChild(this.label);
|
||||
|
||||
this.line.addEventListener("contextmenu", (ev) => {
|
||||
this.select();
|
||||
this.openContextMenu(ev);
|
||||
}, false);
|
||||
|
||||
this.children = doc.createElementNS(HTML_NS, "ul");
|
||||
this.children.classList.add("children");
|
||||
|
||||
this.elt.appendChild(this.children);
|
||||
|
||||
this.line.addEventListener("click", (evt) => {
|
||||
if (!this.selected) {
|
||||
this.select();
|
||||
this.expanded = true;
|
||||
evt.stopPropagation();
|
||||
}
|
||||
}, false);
|
||||
this.expander.addEventListener("click", (evt) => {
|
||||
this.expanded = !this.expanded;
|
||||
this.select();
|
||||
evt.stopPropagation();
|
||||
}, true);
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.elt.remove();
|
||||
this.expander.remove();
|
||||
this.icon.remove();
|
||||
this.highlighter.remove();
|
||||
this.children.remove();
|
||||
this.label.remove();
|
||||
this.elt = this.expander = this.icon = this.highlighter = this.children = this.label = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the context menu when right clicking on the view.
|
||||
* XXX: We could pass this to plugins to allow themselves
|
||||
* to be register/remove items from the context menu if needed.
|
||||
*
|
||||
* @param Event e
|
||||
*/
|
||||
openContextMenu: function(ev) {
|
||||
ev.preventDefault();
|
||||
let popup = this.tree.doc.getElementById("directory-menu-popup");
|
||||
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the view based on the current state of the Resource.
|
||||
*/
|
||||
update: function() {
|
||||
let visible = this.tree.options.resourceVisible ?
|
||||
this.tree.options.resourceVisible(this.resource) :
|
||||
true;
|
||||
|
||||
this.elt.hidden = !visible;
|
||||
|
||||
this.tree.options.resourceFormatter(this.resource, this.label);
|
||||
|
||||
this.icon.className = "file-icon";
|
||||
|
||||
let contentCategory = this.resource.contentCategory;
|
||||
let baseName = this.resource.basename || "";
|
||||
|
||||
if (!this.resource.parent) {
|
||||
this.icon.classList.add("icon-none");
|
||||
} else if (this.resource.isDir) {
|
||||
this.icon.classList.add("icon-folder");
|
||||
} else if (baseName.endsWith(".manifest") || baseName.endsWith(".webapp")) {
|
||||
this.icon.classList.add("icon-manifest");
|
||||
} else if (contentCategory === "js") {
|
||||
this.icon.classList.add("icon-js");
|
||||
} else if (contentCategory === "css") {
|
||||
this.icon.classList.add("icon-css");
|
||||
} else if (contentCategory === "html") {
|
||||
this.icon.classList.add("icon-html");
|
||||
} else if (contentCategory === "image") {
|
||||
this.icon.classList.add("icon-img");
|
||||
} else {
|
||||
this.icon.classList.add("icon-file");
|
||||
}
|
||||
|
||||
this.expander.style.visibility = this.resource.hasChildren ? "visible" : "hidden";
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Select this view in the ProjectTreeView.
|
||||
*/
|
||||
select: function() {
|
||||
this.tree.selectContainer(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns Boolean
|
||||
* Is this view currently selected
|
||||
*/
|
||||
get selected() {
|
||||
return this.line.classList.contains("selected");
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the selected state in the UI.
|
||||
*/
|
||||
set selected(v) {
|
||||
if (v) {
|
||||
this.line.classList.add("selected");
|
||||
} else {
|
||||
this.line.classList.remove("selected");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns Boolean
|
||||
* Are any children visible.
|
||||
*/
|
||||
get expanded() {
|
||||
return !this.elt.classList.contains("tree-collapsed");
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the visiblity state of children.
|
||||
*/
|
||||
set expanded(v) {
|
||||
if (v) {
|
||||
this.elt.classList.remove("tree-collapsed");
|
||||
this.expander.setAttribute("open", "");
|
||||
} else {
|
||||
this.expander.removeAttribute("open");
|
||||
this.elt.classList.add("tree-collapsed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* TreeView is a view managing a list of children.
|
||||
* It is not to be instantiated directly - only extended.
|
||||
* Use ProjectTreeView instead.
|
||||
*/
|
||||
var TreeView = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* @param Document document
|
||||
* @param Object options
|
||||
* - resourceFormatter: a function(Resource, DOMNode)
|
||||
* that renders the resource into the view
|
||||
* - resourceVisible: a function(Resource) -> Boolean
|
||||
* that determines if the resource should show up.
|
||||
*/
|
||||
initialize: function(document, options) {
|
||||
this.doc = document;
|
||||
this.options = merge({
|
||||
resourceFormatter: function(resource, elt) {
|
||||
elt.textContent = resource.toString();
|
||||
}
|
||||
}, options);
|
||||
this.models = new Set();
|
||||
this.roots = new Set();
|
||||
this._containers = new Map();
|
||||
this.elt = document.createElement("vbox");
|
||||
this.elt.tree = this;
|
||||
this.elt.className = "side-menu-widget-container sources-tree";
|
||||
this.elt.setAttribute("with-arrows", "true");
|
||||
this.elt.setAttribute("theme", "dark");
|
||||
this.elt.setAttribute("flex", "1");
|
||||
|
||||
this.children = document.createElementNS(HTML_NS, "ul");
|
||||
this.children.setAttribute("flex", "1");
|
||||
this.elt.appendChild(this.children);
|
||||
|
||||
this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
|
||||
this.updateResource = this.updateResource.bind(this);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._destroyed = true;
|
||||
this.elt.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompt the user to create a new file in the tree.
|
||||
*
|
||||
* @param string initial
|
||||
* The suggested starting file name
|
||||
* @param Resource parent
|
||||
* @param Resource sibling
|
||||
* Which resource to put this next to. If not set,
|
||||
* it will be put in front of all other children.
|
||||
*
|
||||
* @returns Promise
|
||||
* Resolves once the prompt has been successful,
|
||||
* Rejected if it is cancelled
|
||||
*/
|
||||
promptNew: function(initial, parent, sibling=null) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
let parentContainer = this._containers.get(parent);
|
||||
let item = this.doc.createElement("li");
|
||||
item.className = "child";
|
||||
let placeholder = this.doc.createElementNS(HTML_NS, "div");
|
||||
placeholder.className = "child";
|
||||
item.appendChild(placeholder);
|
||||
|
||||
let children = parentContainer.children;
|
||||
sibling = sibling ? this._containers.get(sibling).elt : null;
|
||||
parentContainer.children.insertBefore(item, sibling ? sibling.nextSibling : children.firstChild);
|
||||
|
||||
new InplaceEditor({
|
||||
element: placeholder,
|
||||
initial: initial,
|
||||
start: editor => {
|
||||
editor.input.select();
|
||||
},
|
||||
done: function(val, commit) {
|
||||
if (commit) {
|
||||
deferred.resolve(val);
|
||||
} else {
|
||||
deferred.reject(val);
|
||||
}
|
||||
parentContainer.line.focus();
|
||||
},
|
||||
destroy: () => {
|
||||
item.parentNode.removeChild(item);
|
||||
},
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new Store into the TreeView
|
||||
*
|
||||
* @param Store model
|
||||
*/
|
||||
addModel: function(model) {
|
||||
if (this.models.has(model)) {
|
||||
// Requesting to add a model that already exists
|
||||
return;
|
||||
}
|
||||
this.models.add(model);
|
||||
let placeholder = this.doc.createElementNS(HTML_NS, "li");
|
||||
placeholder.style.display = "none";
|
||||
this.children.appendChild(placeholder);
|
||||
this.roots.add(model.root);
|
||||
model.root.refresh().then(root => {
|
||||
if (this._destroyed || !this.models.has(model)) {
|
||||
// model may have been removed during the initial refresh.
|
||||
// In this case, do not import the resource or add to DOM, just leave it be.
|
||||
return;
|
||||
}
|
||||
let container = this.importResource(root);
|
||||
container.line.classList.add("side-menu-widget-group-title");
|
||||
container.line.setAttribute("theme", "dark");
|
||||
this.selectContainer(container);
|
||||
|
||||
this.children.insertBefore(container.elt, placeholder);
|
||||
this.children.removeChild(placeholder);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a Store from the TreeView
|
||||
*
|
||||
* @param Store model
|
||||
*/
|
||||
removeModel: function(model) {
|
||||
this.models.delete(model);
|
||||
this.removeResource(model.root);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the ResourceContainer. Used for testing the view.
|
||||
*
|
||||
* @param Resource resource
|
||||
* @returns ResourceContainer
|
||||
*/
|
||||
getViewContainer: function(resource) {
|
||||
return this._containers.get(resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a ResourceContainer in the tree.
|
||||
*
|
||||
* @param ResourceContainer container
|
||||
*/
|
||||
selectContainer: function(container) {
|
||||
if (this.selectedContainer === container) {
|
||||
return;
|
||||
}
|
||||
if (this.selectedContainer) {
|
||||
this.selectedContainer.selected = false;
|
||||
}
|
||||
this.selectedContainer = container;
|
||||
container.selected = true;
|
||||
emit(this, "selection", container.resource);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a Resource in the tree.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
selectResource: function(resource) {
|
||||
this.selectContainer(this._containers.get(resource));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the currently selected Resource
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
getSelectedResource: function() {
|
||||
return this.selectedContainer.resource;
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert a Resource into the view.
|
||||
* Makes a new ResourceContainer if needed
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
importResource: function(resource) {
|
||||
if (!resource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._containers.has(resource)) {
|
||||
return this._containers.get(resource);
|
||||
}
|
||||
var container = ResourceContainer(this, resource);
|
||||
this._containers.set(resource, container);
|
||||
this._updateChildren(container);
|
||||
|
||||
on(this, resource, "children-changed", this.resourceChildrenChanged);
|
||||
on(this, resource, "label-change", this.updateResource);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a Resource from the FileSystem. XXX: This should
|
||||
* definitely be moved away from here, maybe to the store?
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
deleteResource: function(resource) {
|
||||
if (resource.isDir) {
|
||||
return OS.File.removeDir(resource.path);
|
||||
} else {
|
||||
return OS.File.remove(resource.path);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a Resource (including children) from the view.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
removeResource: function(resource) {
|
||||
let toRemove = resource.allDescendants();
|
||||
toRemove.add(resource);
|
||||
for (let remove of toRemove) {
|
||||
this._removeResource(remove);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an individual Resource (but not children) from the view.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
_removeResource: function(resource) {
|
||||
resource.off("children-changed", this.resourceChildrenChanged);
|
||||
resource.off("label-change", this.updateResource);
|
||||
if (this._containers.get(resource)) {
|
||||
this._containers.get(resource).destroy();
|
||||
this._containers.delete(resource);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for when a resource has new children.
|
||||
* This can happen as files are being loaded in from FileSystem, for example.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
resourceChildrenChanged: function(resource) {
|
||||
this.updateResource(resource);
|
||||
this._updateChildren(this._containers.get(resource));
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for when a label in the view has been updated.
|
||||
* For example, the 'dirty' plugin marks changed files with an '*'
|
||||
* next to the filename, and notifies with this event.
|
||||
*
|
||||
* @param Resource resource
|
||||
*/
|
||||
updateResource: function(resource) {
|
||||
let container = this._containers.get(resource);
|
||||
container.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Build necessary ResourceContainers for a Resource and its
|
||||
* children, then append them into the view.
|
||||
*
|
||||
* @param ResourceContainer container
|
||||
*/
|
||||
_updateChildren: function(container) {
|
||||
let resource = container.resource;
|
||||
let fragment = this.doc.createDocumentFragment();
|
||||
if (resource.children) {
|
||||
for (let child of resource.childrenSorted) {
|
||||
let childContainer = this.importResource(child);
|
||||
fragment.appendChild(childContainer.elt);
|
||||
}
|
||||
}
|
||||
|
||||
while (container.children.firstChild) {
|
||||
container.children.firstChild.remove();
|
||||
}
|
||||
|
||||
container.children.appendChild(fragment);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* ProjectTreeView is the implementation of TreeView
|
||||
* that is exported. This is the class that is to be used
|
||||
* directly.
|
||||
*/
|
||||
var ProjectTreeView = Class({
|
||||
extends: TreeView,
|
||||
|
||||
/**
|
||||
* See TreeView.initialize
|
||||
*
|
||||
* @param Document document
|
||||
* @param Object options
|
||||
*/
|
||||
initialize: function(document, options) {
|
||||
TreeView.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.forgetProject();
|
||||
TreeView.prototype.destroy.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove current project and empty the tree
|
||||
*/
|
||||
forgetProject: function() {
|
||||
if (this.project) {
|
||||
forget(this, this.project);
|
||||
for (let store of this.project.allStores()) {
|
||||
this.removeModel(store);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a project in the tree
|
||||
*
|
||||
* @param Project project
|
||||
* The project to render into a tree
|
||||
*/
|
||||
setProject: function(project) {
|
||||
this.forgetProject();
|
||||
this.project = project;
|
||||
if (this.project) {
|
||||
on(this, project, "store-added", this.addModel.bind(this));
|
||||
on(this, project, "store-removed", this.removeModel.bind(this));
|
||||
on(this, project, "project-saved", this.refresh.bind(this));
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the tree with all of the current project stores
|
||||
*/
|
||||
refresh: function() {
|
||||
for (let store of this.project.allStores()) {
|
||||
this.addModel(store);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.ProjectTreeView = ProjectTreeView;
|
|
@ -0,0 +1,6 @@
|
|||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
TEST_DIRS += ['test']
|
|
@ -0,0 +1,13 @@
|
|||
[DEFAULT]
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
head.js
|
||||
helper_homepage.html
|
||||
|
||||
[browser_projecteditor_delete_file.js]
|
||||
[browser_projecteditor_editing_01.js]
|
||||
[browser_projecteditor_immediate_destroy.js]
|
||||
[browser_projecteditor_init.js]
|
||||
[browser_projecteditor_new_file.js]
|
||||
[browser_projecteditor_stores.js]
|
||||
[browser_projecteditor_tree_selection.js]
|
|
@ -0,0 +1,80 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test tree selection functionality
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory();
|
||||
ok(true, "ProjectEditor has loaded");
|
||||
|
||||
let root = [...projecteditor.project.allStores()][0].root;
|
||||
is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
|
||||
for (let child of root.children) {
|
||||
yield deleteWithContextMenu(projecteditor.projectTree.getViewContainer(child));
|
||||
}
|
||||
|
||||
function onPopupShow(contextMenu) {
|
||||
let defer = promise.defer();
|
||||
contextMenu.addEventListener("popupshown", function onpopupshown() {
|
||||
contextMenu.removeEventListener("popupshown", onpopupshown);
|
||||
defer.resolve();
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function onPopupHide(contextMenu) {
|
||||
let defer = promise.defer();
|
||||
contextMenu.addEventListener("popuphidden", function popuphidden() {
|
||||
contextMenu.removeEventListener("popuphidden", popuphidden);
|
||||
defer.resolve();
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function openContextMenuOn(node) {
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
node,
|
||||
{button: 2, type: "contextmenu"},
|
||||
node.ownerDocument.defaultView
|
||||
);
|
||||
}
|
||||
|
||||
function deleteWithContextMenu(container) {
|
||||
let defer = promise.defer();
|
||||
|
||||
let resource = container.resource;
|
||||
let popup = projecteditor.document.getElementById("directory-menu-popup");
|
||||
info ("Going to attempt deletion for: " + resource.path)
|
||||
|
||||
onPopupShow(popup).then(function () {
|
||||
let deleteCommand = popup.querySelector("[command=cmd-delete]");
|
||||
ok (deleteCommand, "Delete command exists in popup");
|
||||
is (deleteCommand.getAttribute("hidden"), "", "Delete command is visible");
|
||||
is (deleteCommand.getAttribute("disabled"), "", "Delete command is enabled");
|
||||
|
||||
onPopupHide(popup).then(() => {
|
||||
ok (true, "Popup has been hidden, waiting for project refresh");
|
||||
projecteditor.project.refresh().then(() => {
|
||||
OS.File.stat(resource.path).then(() => {
|
||||
ok (false, "The file was not deleted");
|
||||
defer.resolve();
|
||||
}, (ex) => {
|
||||
ok (ex instanceof OS.File.Error && ex.becauseNoSuchFile, "OS.File.stat promise was rejected because the file is gone");
|
||||
defer.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
deleteCommand.click();
|
||||
popup.hidePopup();
|
||||
});
|
||||
|
||||
openContextMenuOn(container.label);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test ProjectEditor basic functionality
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory();
|
||||
let TEMP_PATH = [...projecteditor.project.allPaths()][0];
|
||||
|
||||
is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
|
||||
|
||||
ok (projecteditor.currentEditor, "There is an editor for projecteditor");
|
||||
let resources = projecteditor.project.allResources();
|
||||
|
||||
resources.forEach((r, i) => {
|
||||
console.log("Resource detected", r.path, i);
|
||||
});
|
||||
|
||||
let stylesCss = resources.filter(r=>r.basename === "styles.css")[0];
|
||||
yield selectFile(projecteditor, stylesCss);
|
||||
yield testEditFile(projecteditor, getTempFile("css/styles.css").path, "body,html { color: orange; }");
|
||||
|
||||
let indexHtml = resources.filter(r=>r.basename === "index.html")[0];
|
||||
yield selectFile(projecteditor, indexHtml);
|
||||
yield testEditFile(projecteditor, getTempFile("index.html").path, "<h1>Changed Content Again</h1>");
|
||||
|
||||
let license = resources.filter(r=>r.basename === "LICENSE")[0];
|
||||
yield selectFile(projecteditor, license);
|
||||
yield testEditFile(projecteditor, getTempFile("LICENSE").path, "My new license");
|
||||
|
||||
let readmeMd = resources.filter(r=>r.basename === "README.md")[0];
|
||||
yield selectFile(projecteditor, readmeMd);
|
||||
yield testEditFile(projecteditor, getTempFile("README.md").path, "My new license");
|
||||
|
||||
let scriptJs = resources.filter(r=>r.basename === "script.js")[0];
|
||||
yield selectFile(projecteditor, scriptJs);
|
||||
yield testEditFile(projecteditor, getTempFile("js/script.js").path, "alert('hi')");
|
||||
|
||||
let vectorSvg = resources.filter(r=>r.basename === "vector.svg")[0];
|
||||
yield selectFile(projecteditor, vectorSvg);
|
||||
yield testEditFile(projecteditor, getTempFile("img/icons/vector.svg").path, "<svg></svg>");
|
||||
});
|
||||
|
||||
function selectFile (projecteditor, resource) {
|
||||
ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
|
||||
projecteditor.projectTree.selectResource(resource);
|
||||
|
||||
if (resource.isDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
let [editorActivated] = yield promise.all([
|
||||
onceEditorActivated(projecteditor)
|
||||
]);
|
||||
|
||||
is (editorActivated, projecteditor.currentEditor, "Editor has been activated for " + resource.path);
|
||||
}
|
||||
|
||||
function testEditFile(projecteditor, filePath, newData) {
|
||||
info ("Testing file editing for: " + filePath);
|
||||
|
||||
let initialData = yield getFileData(filePath);
|
||||
let editor = projecteditor.currentEditor;
|
||||
let resource = projecteditor.resourceFor(editor);
|
||||
let viewContainer= projecteditor.projectTree.getViewContainer(resource);
|
||||
let originalTreeLabel = viewContainer.label.textContent;
|
||||
|
||||
is (resource.path, filePath, "Resource path is set correctly");
|
||||
is (editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
|
||||
|
||||
info ("Setting text in the editor and doing checks before saving");
|
||||
|
||||
editor.editor.setText(newData);
|
||||
is (editor.editor.getText(), newData, "Editor has been filled with new data");
|
||||
is (viewContainer.label.textContent, "*" + originalTreeLabel, "Label is marked as changed");
|
||||
|
||||
info ("Saving the editor and checking to make sure the file gets saved on disk");
|
||||
|
||||
editor.save(resource);
|
||||
|
||||
let savedResource = yield onceEditorSave(projecteditor);
|
||||
|
||||
is (viewContainer.label.textContent, originalTreeLabel, "Label is unmarked as changed");
|
||||
is (savedResource.path, filePath, "The saved resouce path matches the original file path");
|
||||
is (savedResource, resource, "The saved resource is the same as the original resource");
|
||||
|
||||
let savedData = yield getFileData(filePath);
|
||||
is (savedData, newData, "Data has been correctly saved to disk");
|
||||
|
||||
info ("Finished checking saving for " + filePath);
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that projecteditor can be destroyed in various states of loading
|
||||
// without causing any leaks or exceptions.
|
||||
|
||||
let test = asyncTest(function* () {
|
||||
|
||||
info ("Testing tab closure when projecteditor is in various states");
|
||||
|
||||
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
|
||||
let iframe = content.document.getElementById("projecteditor-iframe");
|
||||
ok (iframe, "Tab has placeholder iframe for projecteditor");
|
||||
|
||||
info ("Closing the tab without doing anything");
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
|
||||
let iframe = content.document.getElementById("projecteditor-iframe");
|
||||
ok (iframe, "Tab has placeholder iframe for projecteditor");
|
||||
|
||||
let projecteditor = ProjectEditor.ProjectEditor();
|
||||
ok (projecteditor, "ProjectEditor has been initialized");
|
||||
|
||||
info ("Closing the tab before attempting to load");
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
|
||||
let iframe = content.document.getElementById("projecteditor-iframe");
|
||||
ok (iframe, "Tab has placeholder iframe for projecteditor");
|
||||
|
||||
let projecteditor = ProjectEditor.ProjectEditor();
|
||||
ok (projecteditor, "ProjectEditor has been initialized");
|
||||
|
||||
projecteditor.load(iframe);
|
||||
|
||||
info ("Closing the tab after a load is requested, but before load is finished");
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
|
||||
let iframe = content.document.getElementById("projecteditor-iframe");
|
||||
ok (iframe, "Tab has placeholder iframe for projecteditor");
|
||||
|
||||
let projecteditor = ProjectEditor.ProjectEditor();
|
||||
ok (projecteditor, "ProjectEditor has been initialized");
|
||||
|
||||
return projecteditor.load(iframe).then(() => {
|
||||
info ("Closing the tab after a load has been requested and finished");
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
});
|
||||
|
||||
finish();
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that projecteditor can be initialized.
|
||||
|
||||
function test() {
|
||||
info ("Initializing projecteditor");
|
||||
addProjectEditorTab().then((projecteditor) => {
|
||||
ok (projecteditor, "Load callback has been called");
|
||||
ok (projecteditor.shells, "ProjectEditor has shells");
|
||||
ok (projecteditor.project, "ProjectEditor has a project");
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test tree selection functionality
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory();
|
||||
ok(projecteditor, "ProjectEditor has loaded");
|
||||
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test ProjectEditor basic functionality
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory();
|
||||
let TEMP_PATH = [...projecteditor.project.allPaths()][0];
|
||||
is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
|
||||
|
||||
is ([...projecteditor.project.allPaths()].length, 1, "1 path is set");
|
||||
projecteditor.project.removeAllStores();
|
||||
is ([...projecteditor.project.allPaths()].length, 0, "No paths are remaining");
|
||||
});
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче