merge m-c to fx-team

This commit is contained in:
Tim Taubert 2012-09-10 09:47:01 +02:00
commit 59de8b0c3a
18 changed files with 813 additions and 406 deletions

View File

@ -89,7 +89,7 @@
<command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
<command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
<command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/>
<command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focus();" disabled="true"/>
<command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/>
<command id="Tools:WebConsole" oncommand="HUDConsoleUI.toggleHUD();"/>
<command id="Tools:Inspect" oncommand="InspectorUI.toggleInspectorUI();"/>
<command id="Tools:Debugger" oncommand="DebuggerUI.toggleDebugger();" disabled="true" hidden="true"/>

View File

@ -41,7 +41,7 @@ gcli.addCommand({
manual: gcli.lookup("screenshotFullPageManual")
},
{
name: "node",
name: "selector",
type: "node",
defaultValue: null,
description: gcli.lookup("inspectNodeDesc"),
@ -59,7 +59,7 @@ gcli.addCommand({
return promise;
}
else {
return this.grabScreen(document, args.filename, args.fullpage, args.node);
return this.grabScreen(document, args.filename, args.fullpage, args.selector);
}
},
grabScreen:

View File

@ -1707,7 +1707,7 @@ var types = require('gcli/types');
var Type = require('gcli/types').Type;
var Status = require('gcli/types').Status;
var Conversion = require('gcli/types').Conversion;
var Speller = require('gcli/types/spell').Speller;
var spell = require('gcli/types/spell');
/**
@ -1888,13 +1888,14 @@ SelectionType.prototype._findPredictions = function(arg) {
}
// Try fuzzy matching if we don't get a prefix match
if (false && predictions.length === 0) {
var speller = new Speller();
var names = lookup.map(function(opt) {
return opt.name;
if (predictions.length === 0) {
var names = [];
lookup.forEach(function(opt) {
if (!opt.value.hidden) {
names.push(opt.name);
}
});
speller.train(names);
var corrected = speller.correct(match);
var corrected = spell.correct(match, names);
if (corrected) {
lookup.forEach(function(opt) {
if (opt.name === corrected) {
@ -2005,154 +2006,117 @@ exports.SelectionType = SelectionType;
});
/*
* Copyright (c) 2009 Panagiotis Astithas
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
* Copyright 2012, Mozilla Foundation and contributors
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
define('gcli/types/spell', ['require', 'exports', 'module' ], function(require, exports, module) {
/*
* A spell-checker based on Damerau-Levenshtein distance.
*/
var INSERTION_COST = 1;
var DELETION_COST = 1;
var SWAP_COST = 1;
var SUBSTITUTION_COST = 2;
var MAX_EDIT_DISTANCE = 4;
/**
* A spell-checker based on the statistical algorithm described by Peter Norvig
* in http://norvig.com/spell-correct.html, and converted to JavaScript by Past
* http://past.github.com/speller/
*
* Usage requires a two-step process:
* 1) call speller.train() one or more times with a large text to train the
* language model
* 2) call speller.correct(word) to retrieve the correction for the specified
* word
* Compute Damerau-Levenshtein Distance
* @see http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance
*/
function Speller() {
// A map of words to the count of times they were encountered during training.
this._nWords = {};
}
function damerauLevenshteinDistance(wordi, wordj) {
var N = wordi.length;
var M = wordj.length;
Speller.letters = "abcdefghijklmnopqrstuvwxyz".split("");
// We only need to store three rows of our dynamic programming matrix.
// (Without swap, it would have been two.)
var row0 = new Array(N+1);
var row1 = new Array(N+1);
var row2 = new Array(N+1);
var tmp;
/**
* A function that trains the language model with the words in the supplied
* text. Multiple invocation of this function can extend the training of the
* model.
*/
Speller.prototype.train = function(words) {
words.forEach(function(word) {
word = word.toLowerCase();
this._nWords[word] = this._nWords.hasOwnProperty(word) ?
this._nWords[word] + 1 :
1;
}, this);
var i, j;
// The distance between the empty string and a string of size i is the cost
// of i insertions.
for (i = 0; i <= N; i++) {
row1[i] = i * INSERTION_COST;
}
// Row-by-row, we're computing the edit distance between substrings wordi[0..i]
// and wordj[0..j].
for (j = 1; j <= M; j++)
{
// Edit distance between wordi[0..0] and wordj[0..j] is the cost of j
// insertions.
row0[0] = j * INSERTION_COST;
for (i = 1; i <= N; i++)
{
// Handle deletion, insertion and substitution: we can reach each cell
// from three other cells corresponding to those three operations. We
// want the minimum cost.
row0[i] = Math.min(
row0[i-1] + DELETION_COST,
row1[i] + INSERTION_COST,
row1[i-1] + (wordi[i-1] === wordj[j-1] ? 0 : SUBSTITUTION_COST));
// We handle swap too, eg. distance between help and hlep should be 1. If
// we find such a swap, there's a chance to update row0[1] to be lower.
if (i > 1 && j > 1 && wordi[i-1] === wordj[j-2] && wordj[j-1] === wordi[i-2]) {
row0[i] = Math.min(row0[i], row2[i-2] + SWAP_COST);
}
}
tmp = row2;
row2 = row1;
row1 = row0;
row0 = tmp;
}
return row1[N];
};
/**
* A function that returns the correction for the specified word.
*/
Speller.prototype.correct = function(word) {
if (this._nWords.hasOwnProperty(word)) {
return word;
}
exports.correct = function(word, names) {
var distance = {};
var sorted_candidates;
var candidates = {};
var list = this._edits(word);
list.forEach(function(edit) {
if (this._nWords.hasOwnProperty(edit)) {
candidates[this._nWords[edit]] = edit;
names.forEach(function(candidate) {
distance[candidate] = damerauLevenshteinDistance(word, candidate);
});
sorted_candidates = names.sort(function(worda, wordb) {
if (distance[worda] !== distance[wordb]) {
return distance[worda] - distance[wordb];
} else {
// if the score is the same, always return the first string
// in the lexicographical order
return worda < wordb;
}
}, this);
});
if (this._countKeys(candidates) > 0) {
return candidates[this._max(candidates)];
if (distance[sorted_candidates[0]] <= MAX_EDIT_DISTANCE) {
return sorted_candidates[0];
} else {
return undefined;
}
list.forEach(function(edit) {
this._edits(edit).forEach(function(w) {
if (this._nWords.hasOwnProperty(w)) {
candidates[this._nWords[w]] = w;
}
}, this);
}, this);
return this._countKeys(candidates) > 0 ?
candidates[this._max(candidates)] :
null;
};
/**
* A helper function that counts the keys in the supplied object.
*/
Speller.prototype._countKeys = function(object) {
// return Object.keys(object).length; ?
var count = 0;
for (var attr in object) {
if (object.hasOwnProperty(attr)) {
count++;
}
}
return count;
};
/**
* A helper function that returns the word with the most occurrences in the
* language model, among the supplied candidates.
* @param candidates
*/
Speller.prototype._max = function(candidates) {
var arr = [];
for (var candidate in candidates) {
if (candidates.hasOwnProperty(candidate)) {
arr.push(candidate);
}
}
return Math.max.apply(null, arr);
};
/**
* A function that returns the set of possible corrections of the specified
* word. The edits can be deletions, insertions, alterations or transpositions.
*/
Speller.prototype._edits = function(word) {
var results = [];
// Deletion
for (var i = 0; i < word.length; i++) {
results.push(word.slice(0, i) + word.slice(i + 1));
}
// Transposition
for (i = 0; i < word.length - 1; i++) {
results.push(word.slice(0, i) + word.slice(i + 1, i + 2)
+ word.slice(i, i + 1) + word.slice(i + 2));
}
// Alteration
for (i = 0; i < word.length; i++) {
Speller.letters.forEach(function(l) {
results.push(word.slice(0, i) + l + word.slice(i + 1));
}, this);
}
// Insertion
for (i = 0; i <= word.length; i++) {
Speller.letters.forEach(function(l) {
results.push(word.slice(0, i) + l + word.slice(i));
}, this);
}
return results;
};
exports.Speller = Speller;
});
/*
@ -4068,7 +4032,9 @@ exports._empty = [];
*/
exports.setDocument = function(document) {
doc = document;
exports._empty = doc.querySelectorAll('x>:root');
if (doc != null) {
exports._empty = doc.querySelectorAll('x>:root');
}
};
/**
@ -5127,7 +5093,7 @@ var view = require('gcli/ui/view');
var l10n = require('gcli/l10n');
var canon = require('gcli/canon');
var Promise = require('gcli/promise').Promise;
var Q = require('gcli/promise');
var Status = require('gcli/types').Status;
var Conversion = require('gcli/types').Conversion;
@ -5190,8 +5156,6 @@ function Assignment(param, paramIndex) {
this.paramIndex = paramIndex;
this.onAssignmentChange = util.createEvent('Assignment.onAssignmentChange');
this.setBlank();
}
/**
@ -5275,39 +5239,6 @@ Assignment.prototype.isInName = function() {
this.conversion.arg.prefix.slice(-1) !== ' ';
};
/**
* Report on the status of the last parse() conversion.
* We force mutations to happen through this method rather than have
* setValue and setArgument functions to help maintain integrity when we
* have ArrayArguments and don't want to get confused. This way assignments
* are just containers for a conversion rather than things that store
* a connection between an arg/value.
* @see types.Conversion
*/
Assignment.prototype.setConversion = function(conversion) {
var oldConversion = this.conversion;
this.conversion = conversion;
this.conversion.assign(this);
if (this.conversion.equals(oldConversion)) {
return;
}
this.onAssignmentChange({
assignment: this,
conversion: this.conversion,
oldConversion: oldConversion
});
};
/**
* Setup an empty value for the conversion by parsing an empty argument.
*/
Assignment.prototype.setBlank = function() {
this.setConversion(this.param.type.getBlank());
};
/**
* Make sure that there is some content for this argument by using an
* Argument of '' if needed.
@ -5454,8 +5385,6 @@ function CommandAssignment() {
this.param = new canon.Parameter(commandParamMetadata);
this.paramIndex = -1;
this.onAssignmentChange = util.createEvent('CommandAssignment.onAssignmentChange');
this.setBlank();
}
CommandAssignment.prototype = Object.create(Assignment.prototype);
@ -5540,6 +5469,7 @@ function Requisition(environment, doc) {
// The command that we are about to execute.
// @see setCommandConversion()
this.commandAssignment = new CommandAssignment();
this._setAssignment(this.commandAssignment, null, true);
// The object that stores of Assignment objects that we are filling out.
// The Assignment objects are stored under their param.name for named
@ -5626,6 +5556,7 @@ Requisition.prototype._commandAssignmentChanged = function(ev) {
for (var i = 0; i < command.params.length; i++) {
var param = command.params[i];
var assignment = new Assignment(param, i);
this._setAssignment(assignment, null, true);
assignment.onAssignmentChange.add(this._assignmentChanged, this);
this._assignments[param.name] = assignment;
}
@ -5748,40 +5679,81 @@ Requisition.prototype.getAssignments = function(includeCommand) {
};
/**
* Alter the given assignment using the given arg. This function is better than
* calling assignment.setConversion(assignment.param.type.parse(arg)) because
* it adjusts the args in this requisition to keep things up to date
* Alter the given assignment using the given arg.
* @param assignment The assignment to alter
* @param arg The new value for the assignment. An instance of Argument, or an
* instance of Conversion, or null to set the blank value.
*/
Requisition.prototype.setAssignment = function(assignment, arg) {
var originalArgs = assignment.arg.getArgs();
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
this._setAssignment(assignment, arg, false);
};
var replacementArgs = arg.getArgs();
var maxLen = Math.max(originalArgs.length, replacementArgs.length);
for (var i = 0; i < maxLen; i++) {
// If there are no more original args, or if the original arg was blank
// (i.e. not typed by the user), we'll just need to add at the end
if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') {
this._args.push(replacementArgs[i]);
continue;
}
/**
* Internal function to alter the given assignment using the given arg.
* @param assignment The assignment to alter
* @param arg The new value for the assignment. An instance of Argument, or an
* instance of Conversion, or null to set the blank value.
* @param skipArgUpdate (default=false) Adjusts the args in this requisition to
* keep things up to date. Args should only be skipped when setAssignment is
* being called as part of the update process.
*/
Requisition.prototype._setAssignment = function(assignment, arg, skipArgUpdate) {
if (!skipArgUpdate) {
var originalArgs = assignment.arg.getArgs();
var index = this._args.indexOf(originalArgs[i]);
if (index === -1) {
console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args);
throw new Error('Couldn\'t find ' + originalArgs[i]);
}
// Update the args array
var replacementArgs = arg.getArgs();
var maxLen = Math.max(originalArgs.length, replacementArgs.length);
for (var i = 0; i < maxLen; i++) {
// If there are no more original args, or if the original arg was blank
// (i.e. not typed by the user), we'll just need to add at the end
if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') {
this._args.push(replacementArgs[i]);
continue;
}
// If there are no more replacement args, we just remove the original args
// Otherwise swap original args and replacements
if (i >= replacementArgs.length) {
this._args.splice(index, 1);
}
else {
this._args[index] = replacementArgs[i];
var index = this._args.indexOf(originalArgs[i]);
if (index === -1) {
console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args);
throw new Error('Couldn\'t find ' + originalArgs[i]);
}
// If there are no more replacement args, we just remove the original args
// Otherwise swap original args and replacements
if (i >= replacementArgs.length) {
this._args.splice(index, 1);
}
else {
this._args[index] = replacementArgs[i];
}
}
}
var conversion;
if (arg == null) {
conversion = assignment.param.type.getBlank();
}
else if (typeof arg.getStatus === 'function') {
conversion = arg;
}
else {
conversion = assignment.param.type.parse(arg);
}
var oldConversion = assignment.conversion;
assignment.conversion = conversion;
assignment.conversion.assign(assignment);
if (assignment.conversion.equals(oldConversion)) {
return;
}
assignment.onAssignmentChange({
assignment: assignment,
conversion: assignment.conversion,
oldConversion: oldConversion
});
};
/**
@ -5789,7 +5761,7 @@ Requisition.prototype.setAssignment = function(assignment, arg) {
*/
Requisition.prototype.setBlankArguments = function() {
this.getAssignments().forEach(function(assignment) {
assignment.setBlank();
this._setAssignment(assignment, null, true);
}, this);
};
@ -6148,8 +6120,8 @@ Requisition.prototype.getAssignmentAt = function(cursor) {
* @param input (optional) The command to execute. See above.
*/
Requisition.prototype.exec = function(input) {
var command;
var args;
var command = null;
var args = null;
var hidden = false;
if (input && input.hidden) {
hidden = true;
@ -6200,33 +6172,58 @@ Requisition.prototype.exec = function(input) {
this.commandOutputManager.onOutput({ output: output });
var onDone = function(data) {
output.complete(data);
};
var onError = function(error) {
console.error(error);
output.error = true;
output.complete(error);
};
try {
var context = exports.createExecutionContext(this);
var reply = command.exec(args, context);
if (reply != null && typeof reply.then === 'function') {
reply.then(
function(data) { output.complete(data); },
function(error) { output.error = true; output.complete(error); });
output.promise = reply;
// Add progress to our promise and add a handler for it here
// See bug 659300
}
else {
output.complete(reply);
}
this._then(reply, onDone, onError);
}
catch (ex) {
console.error(ex);
output.error = true;
output.complete(ex);
onError(ex);
}
this.update('');
return output;
};
/**
* Different types of promise have different ways of doing 'then'. This is a
* catch-all so we can ignore the differences. It also handles concrete values
* and calls onDone directly if thing is not a promise.
* @param thing The value to test for 'promiseness'
* @param onDone The action to take if thing is resolved
* @param onError The action to take if thing is rejected
*/
Requisition.prototype._then = function(thing, onDone, onError) {
var then = null;
if (thing != null && typeof thing.then === 'function') {
// Old GCLI style / simple promises with a then function
then = thing.then;
}
else if (thing != null && thing.promise != null &&
typeof thing.promise.then === 'function') {
// Q / Mozilla add-ons style
then = thing.promise.then;
}
if (then != null) {
then(onDone, onError);
}
else {
onDone(thing);
}
};
/**
* Called by the UI when ever the user interacts with a command line input
* @param typed The contents of the input field
@ -6517,7 +6514,7 @@ Requisition.prototype._split = function(args) {
// Special case: if the user enters { console.log('foo'); } then we need to
// use the hidden 'eval' command
conversion = new Conversion(evalCommand, new ScriptArgument());
this.commandAssignment.setConversion(conversion);
this._setAssignment(this.commandAssignment, conversion, true);
return;
}
@ -6525,8 +6522,8 @@ Requisition.prototype._split = function(args) {
while (argsUsed <= args.length) {
var arg = (argsUsed === 1) ?
args[0] :
new MergedArgument(args, 0, argsUsed);
args[0] :
new MergedArgument(args, 0, argsUsed);
conversion = this.commandAssignment.param.type.parse(arg);
// We only want to carry on if this command is a parent command,
@ -6544,7 +6541,7 @@ Requisition.prototype._split = function(args) {
argsUsed++;
}
this.commandAssignment.setConversion(conversion);
this._setAssignment(this.commandAssignment, conversion, true);
for (var i = 0; i < argsUsed; i++) {
args.shift();
@ -6590,11 +6587,8 @@ Requisition.prototype._assign = function(args) {
if (this.assignmentCount === 1) {
var assignment = this.getAssignment(0);
if (assignment.param.type instanceof StringType) {
var arg = (args.length === 1) ?
args[0] :
new MergedArgument(args);
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
var arg = (args.length === 1) ? args[0] : new MergedArgument(args);
this._setAssignment(assignment, arg, true);
return;
}
}
@ -6639,8 +6633,7 @@ Requisition.prototype._assign = function(args) {
arrayArg.addArgument(arg);
}
else {
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
this._setAssignment(assignment, arg, true);
}
}
else {
@ -6657,7 +6650,7 @@ Requisition.prototype._assign = function(args) {
// If not set positionally, and we can't set it non-positionally,
// we have to default it to prevent previous values surviving
if (!assignment.param.isPositionalAllowed) {
assignment.setBlank();
this._setAssignment(assignment, null, true);
return;
}
@ -6674,7 +6667,7 @@ Requisition.prototype._assign = function(args) {
}
else {
if (args.length === 0) {
assignment.setBlank();
this._setAssignment(assignment, null, true);
}
else {
var arg = args.splice(0, 1)[0];
@ -6688,8 +6681,7 @@ Requisition.prototype._assign = function(args) {
this._unassigned.push(new UnassignedAssignment(this, arg));
}
else {
var conversion = assignment.param.type.parse(arg);
assignment.setConversion(conversion);
this._setAssignment(assignment, arg, true);
}
}
}
@ -6698,8 +6690,7 @@ Requisition.prototype._assign = function(args) {
// Now we need to assign the array argument (if any)
Object.keys(arrayArgs).forEach(function(name) {
var assignment = this.getAssignment(name);
var conversion = assignment.param.type.parse(arrayArgs[name]);
assignment.setConversion(conversion);
this._setAssignment(assignment, arrayArgs[name], true);
}, this);
// What's left is can't be assigned, but we need to extract
@ -6729,17 +6720,31 @@ function Output(options) {
}
/**
* Called when there is data to display
* @param data
* Called when there is data to display, but the command is still executing
* @param data The new data. If the data structure has been altered but the
* root object is still the same, The same root object should be passed in the
* data parameter.
* @param ev Optional additional event data, for example to explain how the
* data structure has changed
*/
Output.prototype.complete = function(data) {
Output.prototype.changed = function(data, ev) {
this.data = data;
ev = ev || {};
ev.output = this;
this.onChange(ev);
};
/**
* Called when there is data to display, and the command has finished executing
* See changed() for details on parameters.
*/
Output.prototype.complete = function(data, ev) {
this.end = new Date();
this.duration = this.end.getTime() - this.start.getTime();
this.completed = true;
this.onChange({ output: this });
this.changed(data, ev);
};
/**
@ -6830,8 +6835,15 @@ exports.createExecutionContext = function(requisition) {
document: requisition.document,
environment: requisition.environment,
createView: view.createView,
defer: function() {
return Q.defer();
},
/**
* @deprecated Use defer() instead, which does the same thing, but is not
* confusingly named
*/
createPromise: function() {
return new Promise();
return Q.defer();
}
};
};
@ -6856,8 +6868,13 @@ exports.createExecutionContext = function(requisition) {
define('gcli/promise', ['require', 'exports', 'module' ], function(require, exports, module) {
Components.utils.import("resource:///modules/devtools/Promise.jsm");
exports.Promise = Promise;
var imported = {};
Components.utils.import("resource://gre/modules/commonjs/promise/core.js",
imported);
exports.defer = imported.Promise.defer;
exports.resolve = imported.Promise.resolve;
exports.reject = imported.Promise.reject;
});
define("text!gcli/ui/intro.html", [], "\n" +
@ -7237,7 +7254,7 @@ FocusManager.prototype._checkShow = function() {
if (fire) {
if (this._debug) {
console.debug('FocusManager.onVisibilityChange', ev);
console.log('FocusManager.onVisibilityChange', ev);
}
this.onVisibilityChange(ev);
}
@ -7735,6 +7752,9 @@ Field.prototype.destroy = function() {
delete this.messageElement;
};
// Note: We could/should probably change Fields from working with Conversions
// to working with Arguments (Tokens), which makes for less calls to parse()
/**
* Update this field display with the value from this conversion.
* Subclasses should provide an implementation of this function.
@ -9393,7 +9413,9 @@ Inputter.prototype.textChanged = function() {
input.typed = newStr;
this._processCaretChange(input);
this.element.value = newStr;
if (this.element.value !== newStr) {
this.element.value = newStr;
}
this.onInputChange({ inputState: input });
};
@ -9469,8 +9491,12 @@ Inputter.prototype._processCaretChange = function(input) {
cursor: { start: start, end: end }
};
this.element.selectionStart = start;
this.element.selectionEnd = end;
if (this.element.selectionStart !== start) {
this.element.selectionStart = start;
}
if (this.element.selectionEnd !== end) {
this.element.selectionEnd = end;
}
this._checkAssignment(start);
@ -9605,7 +9631,7 @@ Inputter.prototype.onKeyUp = function(ev) {
// If the user is on a valid value, then we increment the value, but if
// they've typed something that's not right we page through predictions
if (this.assignment.getStatus() === Status.VALID) {
this.requisition.increment(assignment);
this.requisition.increment(this.assignment);
// See notes on focusManager.onInputChange in onKeyDown
if (this.focusManager) {
this.focusManager.onInputChange();
@ -9629,7 +9655,7 @@ Inputter.prototype.onKeyUp = function(ev) {
else {
// See notes above for the UP key
if (this.assignment.getStatus() === Status.VALID) {
this.requisition.decrement(assignment);
this.requisition.decrement(this.assignment);
// See notes on focusManager.onInputChange in onKeyDown
if (this.focusManager) {
this.focusManager.onInputChange();
@ -10314,7 +10340,7 @@ Tooltip.prototype.selectChoice = function(ev) {
* Called by the onFieldChange event on the current Field
*/
Tooltip.prototype.fieldChanged = function(ev) {
this.assignment.setConversion(ev.conversion);
this.requisition.setAssignment(this.assignment, ev.conversion.arg);
var isError = ev.conversion.message != null && ev.conversion.message !== '';
this.focusManager.setError(isError);

View File

@ -163,8 +163,8 @@ function testPrefStatus() {
helpers.setInput('pref list');
helpers.check({
input: 'pref list',
hints: '',
markup: 'EEEEVEEEE',
hints: ' -> pref set',
markup: 'IIIIVIIII',
status: 'ERROR'
});
}

View File

@ -108,8 +108,8 @@ define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gc
* @param options Lookup of options that customize test running. Includes:
* - window (default=undefined) A reference to the DOM window. If left
* undefined then a reduced set of tests will run.
* - isNode (default=false) Are we running under NodeJS, specifically, are we
* using JSDom, which isn't a 100% complete DOM implementation.
* - isJsdom (default=false) Are we running under JSDom, specifically, which
* isn't a 100% complete DOM implementation.
* Some tests are skipped when using NodeJS.
* - display (default=undefined) A reference to a Display implementation.
* A reduced set of tests will run if left undefined
@ -119,12 +119,12 @@ define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gc
* |requisition.exec()| which prevents the display from becoming messed up,
* however use of hideExec restricts the set of tests that are run
*/
exports.run = function(options) {
exports.runAsync = function(options, callback) {
options = options || {};
examiner.mergeDefaultOptions(options);
examiner.reset();
examiner.run(options);
examiner.runAsync(options, callback);
// A better set of default than those specified above, come from the set
// that are passed to run().
@ -155,13 +155,6 @@ define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gc
// setTimeout keeps stack traces clear of RequireJS frames
window.setTimeout(function() {
var options = {
window: window,
display: window.display,
hideExec: true
};
exports.run(options);
window.createDebugCheck = function() {
require([ 'gclitest/helpers' ], function(helpers) {
helpers.setup(options);
@ -198,7 +191,15 @@ define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gc
mockCommands.setup();
});
};
window.testCommands();
var options = {
window: window,
display: window.display,
hideExec: true
};
exports.runAsync(options, function() {
window.testCommands();
});
}, 10);
return {
@ -326,25 +327,6 @@ examiner.mergeDefaultOptions = function(options) {
});
};
/**
* Run the tests defined in the test suite synchronously
*/
examiner.run = function(options) {
Object.keys(examiner.suites).forEach(function(suiteName) {
var suite = examiner.suites[suiteName];
suite.run(options);
}.bind(this));
if (options.detailedResultLog) {
examiner.detailedResultLog();
}
else {
console.log('Completed test suite');
}
return examiner.suites;
};
/**
* Run all the tests asynchronously
*/
@ -477,21 +459,6 @@ Suite.prototype.reset = function() {
}, this);
};
/**
* Run all the tests in this suite synchronously
*/
Suite.prototype.run = function(options) {
if (!this._setup(options)) {
return;
}
Object.keys(this.tests).forEach(function(testName) {
this.tests[testName].run(options);
}, this);
this._shutdown(options);
};
/**
* Run all the tests in this suite asynchronously
*/
@ -636,6 +603,9 @@ function Test(suite, name, func) {
this.func = func;
this.title = name.replace(/^test/, '').replace(/([A-Z])/g, ' $1');
this.outstanding = [];
this.callback = undefined;
this.failures = [];
this.status = stati.notrun;
this.checks = 0;
@ -645,16 +615,20 @@ function Test(suite, name, func) {
* Reset the test to its original state
*/
Test.prototype.reset = function() {
this.outstanding = [];
this.callback = undefined;
this.failures = [];
this.status = stati.notrun;
this.checks = 0;
};
/**
* Run just a single test
* Run all the tests in this suite asynchronously
*/
Test.prototype.run = function(options) {
Test.prototype.runAsync = function(options, callback) {
assert.currentTest = this;
this.callback = callback;
this.status = stati.executing;
this.failures = [];
this.checks = 0;
@ -675,18 +649,20 @@ Test.prototype.run = function(options) {
}
assert.currentTest = null;
this.checkFinish();
};
/**
* Run all the tests in this suite asynchronously
* Check to see if the currently executing test is completed (i.e. the list of
* outstanding tasks has all been completed)
*/
Test.prototype.runAsync = function(options, callback) {
setTimeout(function() {
this.run(options);
if (typeof callback === 'function') {
callback();
Test.prototype.checkFinish = function() {
if (this.outstanding.length == 0) {
if (typeof this.callback === 'function') {
this.callback();
}
}.bind(this), delay);
}
};
/**
@ -892,21 +868,25 @@ define('gclitest/testCanon', ['require', 'exports', 'module' , 'gclitest/helpers
* limitations under the License.
*/
define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert', 'gcli/util'], function(require, exports, module) {
define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert'], function(require, exports, module) {
var test = require('test/assert');
var util = require('gcli/util');
// A copy of this code exists in firefox mochitests; when updated here it
// should be updated there too. Hence the use of an exports synonym for non
// AMD contexts.
var helpers = exports;
helpers._display = undefined;
helpers._options = undefined;
helpers.setup = function(options) {
helpers._options = options;
helpers._display = options.display;
};
helpers.shutdown = function(options) {
helpers._options = undefined;
helpers._display = undefined;
};
@ -1025,6 +1005,16 @@ helpers.setInput = function(typed, cursor) {
if (cursor) {
helpers._display.inputter.setCursor({ start: cursor, end: cursor });
}
else {
// This is a hack because jsdom appears to not handle cursor updates
// in the same way as most browsers.
if (helpers._options.isJsdom) {
helpers._display.inputter.setCursor({
start: typed.length,
end: typed.length
});
}
}
helpers._display.focusManager.onInputChange();
};
@ -1589,7 +1579,7 @@ exports.testElement = function(options) {
test.ok(assign1.arg.type === 'BlankArgument');
test.is(undefined, assign1.value);
if (!options.isNode) {
if (!options.isJsdom) {
update({ typed: 'tse :root', cursor: { start: 9, end: 9 } });
test.is( 'VVVVVVVVV', statuses);
test.is(Status.VALID, status);
@ -1625,7 +1615,7 @@ exports.testElement = function(options) {
test.is(undefined, assign1.value);
}
else {
test.log('Skipping :root test due to jsdom (from isNode)');
test.log('Skipping :root test due to jsdom');
}
update({ typed: 'tse #', cursor: { start: 5, end: 5 } });
@ -2360,7 +2350,7 @@ exports.testActivate = function(options) {
helpers.setInput('tsg d');
helpers.check({
hints: ' [options]'
hints: ' [options] -> ccc'
});
helpers.setInput('tsg aa');
@ -2774,9 +2764,10 @@ var mockDoc = {
* http://opensource.org/licenses/BSD-3-Clause
*/
define('gclitest/testFocus', ['require', 'exports', 'module' , 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
define('gclitest/testFocus', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
var test = require('test/assert');
var helpers = require('gclitest/helpers');
var mockCommands = require('gclitest/mockCommands');
@ -2791,6 +2782,11 @@ exports.shutdown = function(options) {
};
exports.testBasic = function(options) {
if (options.isJsdom) {
test.log('jsdom does not pass on focus events properly, skipping testBasic');
return;
}
helpers.focusInput();
helpers.exec(options, 'help');
@ -3343,9 +3339,8 @@ exports.testHidden = function(options) {
helpers.setInput('tshidde');
helpers.check({
input: 'tshidde',
markup: 'EEEEEEE',
status: 'ERROR',
hints: '',
hints: ' -> tse',
status: 'ERROR'
});
helpers.setInput('tshidden');
@ -3470,10 +3465,11 @@ exports.testHidden = function(options) {
* limitations under the License.
*/
define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers', 'test/assert'], function(require, exports, module) {
define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers', 'test/assert', 'gcli/canon'], function(require, exports, module) {
var helpers = require('gclitest/helpers');
var test = require('test/assert');
var canon = require('gcli/canon');
exports.setup = function(options) {
helpers.setup(options);
@ -3484,8 +3480,8 @@ define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers
};
exports.testIntroStatus = function(options) {
if (options.isFirefox) {
test.log('Skipping testIntroStatus in Firefox.');
if (canon.getCommand('intro') == null) {
test.log('Skipping testIntroStatus; missing intro command.');
return;
}
@ -3507,8 +3503,8 @@ define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers
};
exports.testIntroExec = function(options) {
if (options.isFirefox) {
test.log('Skipping testIntroExec in Firefox.');
if (canon.getCommand('intro') == null) {
test.log('Skipping testIntroStatus; missing intro command.');
return;
}
@ -3683,7 +3679,10 @@ exports.testBasic = function(options) {
input('{ document.title');
check('VVVVVVVVVVVVVVVV', Status.VALID, 'document.title', 0);
test.ok('donteval' in options.window, 'donteval exists');
if (!options.isJsdom) {
// jsdom causes an eval here, maybe that's node/v8?
test.ok('donteval' in options.window, 'donteval exists');
}
input('{ don');
check('VVIII', Status.ERROR, 'don', 'donteval');
@ -3832,12 +3831,12 @@ exports.testComplete = function(options) {
check('{ wind', COMPLETES_TO, '{ window', 0);
check('{ window.docum', COMPLETES_TO, '{ window.document', 0);
// Bug 717228: This fails under node
if (!options.isNode) {
// Bug 717228: This fails under jsdom
if (!options.isJsdom) {
check('{ window.document.titl', COMPLETES_TO, '{ window.document.title ', 0);
}
else {
test.log('Running under Node. Skipping tests due to bug 717228.');
test.log('Skipping tests due to jsdom and bug 717228.');
}
}
};
@ -4008,61 +4007,66 @@ exports.testNode = function(options) {
}
});
helpers.setInput('tse :root');
helpers.check({
input: 'tse :root',
hints: ' [options]',
markup: 'VVVVVVVVV',
cursor: 9,
current: 'node',
status: 'VALID',
args: {
command: { name: 'tse' },
node: { arg: ' :root', status: 'VALID' },
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
if (options.isJsdom) {
test.log('skipping node tests because jsdom');
}
else {
helpers.setInput('tse :root');
helpers.check({
input: 'tse :root',
hints: ' [options]',
markup: 'VVVVVVVVV',
cursor: 9,
current: 'node',
status: 'VALID',
args: {
command: { name: 'tse' },
node: { arg: ' :root', status: 'VALID' },
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
helpers.setInput('tse :root ');
helpers.check({
input: 'tse :root ',
hints: '[options]',
markup: 'VVVVVVVVVV',
cursor: 10,
current: 'node',
status: 'VALID',
args: {
command: { name: 'tse' },
node: { arg: ' :root ', status: 'VALID' },
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
test.is(requisition.getAssignment('node').value.tagName,
'HTML',
'root id');
helpers.setInput('tse :root ');
helpers.check({
input: 'tse :root ',
hints: '[options]',
markup: 'VVVVVVVVVV',
cursor: 10,
current: 'node',
status: 'VALID',
args: {
command: { name: 'tse' },
node: { arg: ' :root ', status: 'VALID' },
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
test.is(requisition.getAssignment('node').value.tagName,
'HTML',
'root id');
helpers.setInput('tse #gcli-nomatch');
helpers.check({
input: 'tse #gcli-nomatch',
hints: ' [options]',
markup: 'VVVVIIIIIIIIIIIII',
cursor: 17,
current: 'node',
status: 'ERROR',
args: {
command: { name: 'tse' },
node: {
value: undefined,
arg: ' #gcli-nomatch',
status: 'INCOMPLETE',
message: 'No matches'
},
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
helpers.setInput('tse #gcli-nomatch');
helpers.check({
input: 'tse #gcli-nomatch',
hints: ' [options]',
markup: 'VVVVIIIIIIIIIIIII',
cursor: 17,
current: 'node',
status: 'ERROR',
args: {
command: { name: 'tse' },
node: {
value: undefined,
arg: ' #gcli-nomatch',
status: 'INCOMPLETE',
message: 'No matches'
},
nodes: { status: 'VALID' },
nodes2: { status: 'VALID' }
}
});
}
helpers.setInput('tse #');
helpers.check({
@ -4131,6 +4135,11 @@ exports.testNode = function(options) {
exports.testNodes = function(options) {
var requisition = options.display.requisition;
if (options.isJsdom) {
test.log('skipping node tests because jsdom');
return;
}
helpers.setInput('tse :root --nodes *');
helpers.check({
input: 'tse :root --nodes *',
@ -4236,13 +4245,14 @@ exports.testNodes = function(options) {
* limitations under the License.
*/
define('gclitest/testPref', ['require', 'exports', 'module' , 'gcli/commands/pref', 'gclitest/helpers', 'gclitest/mockSettings', 'test/assert'], function(require, exports, module) {
define('gclitest/testPref', ['require', 'exports', 'module' , 'gcli/commands/pref', 'gclitest/helpers', 'gclitest/mockSettings', 'test/assert', 'gcli/canon'], function(require, exports, module) {
var pref = require('gcli/commands/pref');
var helpers = require('gclitest/helpers');
var mockSettings = require('gclitest/mockSettings');
var test = require('test/assert');
var canon = require('gcli/canon');
exports.setup = function(options) {
@ -4270,6 +4280,11 @@ exports.testPrefShowStatus = function(options) {
return;
}
if (canon.getCommand('intro') == null) {
test.log('Skipping testIntroStatus; missing intro command.');
return;
}
helpers.setInput('pref s');
helpers.check({
typed: 'pref s',
@ -4333,6 +4348,11 @@ exports.testPrefSetStatus = function(options) {
return;
}
if (canon.getCommand('intro') == null) {
test.log('Skipping testIntroStatus; missing intro command.');
return;
}
helpers.setInput('pref s');
helpers.check({
typed: 'pref s',
@ -4352,7 +4372,7 @@ exports.testPrefSetStatus = function(options) {
helpers.setInput('pref xxx');
helpers.check({
typed: 'pref xxx',
markup: 'EEEEVEEE',
markup: 'IIIIVIII',
status: 'ERROR'
});
@ -4395,6 +4415,11 @@ exports.testPrefExec = function(options) {
return;
}
if (canon.getCommand('intro') == null) {
test.log('Skipping testIntroStatus; missing intro command.');
return;
}
var initialAllowSet = pref.allowSet.value;
pref.allowSet.value = false;
@ -4744,12 +4769,11 @@ exports.testPredictions = function(options) {
var resource3 = types.getType({ name: 'resource', include: 'text/css' });
var options3 = resource3.getLookup();
// jsdom fails to support digging into stylesheets
if (!options.isNode) {
if (!options.isJsdom) {
test.ok(options3.length >= 1, 'have resources');
}
else {
test.log('Running under Node. ' +
'Skipping checks due to jsdom document.stylsheets support.');
test.log('Skipping checks due to jsdom document.stylsheets support.');
}
options3.forEach(function(prediction) {
checkPrediction(resource3, prediction);
@ -5018,7 +5042,7 @@ exports.testChange = function(options) {
define('gclitest/testSpell', ['require', 'exports', 'module' , 'test/assert', 'gcli/types/spell'], function(require, exports, module) {
var test = require('test/assert');
var Speller = require('gcli/types/spell').Speller;
var spell = require('gcli/types/spell');
exports.setup = function() {
};
@ -5032,15 +5056,14 @@ exports.testSpellerSimple = function(options) {
return;
}
var speller = new Speller();
speller.train(Object.keys(options.window));
var alternatives = Object.keys(options.window);
test.is(speller.correct('document'), 'document');
test.is(speller.correct('documen'), 'document');
test.is(speller.correct('ocument'), 'document');
test.is(speller.correct('odcument'), 'document');
test.is(spell.correct('document', alternatives), 'document');
test.is(spell.correct('documen', alternatives), 'document');
test.is(spell.correct('ocument', alternatives), 'document');
test.is(spell.correct('odcument', alternatives), 'document');
test.is(speller.correct('========='), null);
test.is(spell.correct('=========', alternatives), undefined);
};
@ -5458,7 +5481,7 @@ function type(typed, tests, options) {
inputter.setCursor({ start: tests.cursor, end: tests.cursor });
}
if (!options.isNode) {
if (!options.isJsdom) {
if (tests.important) {
test.ok(tooltip.field.isImportant, 'Important for ' + typed);
}
@ -5488,8 +5511,8 @@ exports.testActivate = function(options) {
return;
}
if (options.isNode) {
test.log('Running under Node. Reduced checks due to JSDom.textContent');
if (options.isJsdom) {
test.log('Reduced checks due to JSDom.textContent');
}
type(' ', { }, options);
@ -5506,11 +5529,9 @@ exports.testActivate = function(options) {
type('tsb tt', {
important: true,
options: [ ],
error: 'Can\'t use \'tt\'.'
options: [ 'true' ]
}, options);
type('asdf', {
important: false,
options: [ ],
@ -5572,9 +5593,8 @@ function forEachType(options, callback) {
}
exports.testDefault = function(options) {
if (options.isNode) {
test.log('Running under Node. ' +
'Skipping tests due to issues with resource type.');
if (options.isJsdom) {
test.log('Skipping tests due to issues with resource type.');
return;
}

View File

@ -35,6 +35,7 @@ const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
const BUTTON_POSITION_SAVE = 0;
const BUTTON_POSITION_CANCEL = 1;
const BUTTON_POSITION_DONT_SAVE = 2;
const BUTTON_POSITION_REVERT=0;
let keysbundle = Services.strings.createBundle("chrome://global-platform/locale/platformKeys.properties");
@ -922,6 +923,72 @@ var Scratchpad = {
}
},
/**
* Restore content from saved version of current file.
*
* @param function aCallback
* Optional function you want to call when file is saved
*/
revertFile: function SP_revertFile(aCallback)
{
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(this.filename);
if (!file.exists()) {
return;
}
this.importFromFile(file, false, function(aStatus, aContent) {
if (aCallback) {
aCallback(aStatus);
}
});
},
/**
* Prompt to revert scratchpad if it has unsaved changes.
*
* @param function aCallback
* Optional function you want to call when file is saved. The callback
* receives three arguments:
* - aRevert (boolean) - tells if the file has been reverted.
* - status (number) - the file revert status result (if the file was
* saved).
*/
promptRevert: function SP_promptRervert(aCallback)
{
if (this.filename) {
let ps = Services.prompt;
let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
let button = ps.confirmEx(window,
this.strings.GetStringFromName("confirmRevert.title"),
this.strings.GetStringFromName("confirmRevert"),
flags, null, null, null, null, {});
if (button == BUTTON_POSITION_CANCEL) {
if (aCallback) {
aCallback(false);
}
return;
}
if (button == BUTTON_POSITION_REVERT) {
this.revertFile(function(aStatus) {
if(aCallback){
aCallback(true, aStatus);
}
});
return;
}
}
if (aCallback) {
aCallback(false);
}
},
/**
* Open the Error Console.
*/
@ -1107,6 +1174,14 @@ var Scratchpad = {
_onDirtyChanged: function SP__onDirtyChanged(aEvent)
{
Scratchpad._updateTitle();
if (Scratchpad.filename) {
if (Scratchpad.editor.dirty) {
document.getElementById("sp-cmd-revert").removeAttribute("disabled");
}
else {
document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
}
}
},
/**

View File

@ -33,6 +33,7 @@
<command id="sp-cmd-clearRecentFiles" oncommand="Scratchpad.clearRecentFiles();"/>
<command id="sp-cmd-save" oncommand="Scratchpad.saveFile();"/>
<command id="sp-cmd-saveas" oncommand="Scratchpad.saveFileAs();"/>
<command id="sp-cmd-revert" oncommand="Scratchpad.promptRevert();" disabled="true"/>
<!-- TODO: bug 650340 - implement printFile()
<command id="sp-cmd-printFile" oncommand="Scratchpad.printFile();" disabled="true"/>
@ -132,6 +133,10 @@
label="&saveFileAsCmd.label;"
accesskey="&saveFileAsCmd.accesskey;"
command="sp-cmd-saveas"/>
<menuitem id="sp-menu-revert"
label="&revertCmd.label;"
accesskey="&revertCmd.accesskey;"
command="sp-cmd-revert"/>
<menuseparator/>
<!-- TODO: bug 650340 - implement printFile

View File

@ -33,6 +33,7 @@ MOCHITEST_BROWSER_FILES = \
browser_scratchpad_bug_650760_help_key.js \
browser_scratchpad_bug_651942_recent_files.js \
browser_scratchpad_bug756681_display_non_error_exceptions.js \
browser_scratchpad_bug_751744_revert_to_saved.js \
head.js \
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,137 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let tempScope = {};
Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
let NetUtil = tempScope.NetUtil;
let FileUtils = tempScope.FileUtils;
// Reference to the Scratchpad object.
let gScratchpad;
// Reference to the temporary nsIFiles.
let gFile;
// Temporary file name.
let gFileName = "testFileForBug751744.tmp"
// Content for the temporary file.
let gFileContent = "/* this file is already saved */\n" +
"function foo() { alert('bar') }";
let gLength = gFileContent.length;
// Reference to the menu entry.
let menu;
function startTest()
{
gScratchpad = gScratchpadWindow.Scratchpad;
menu = gScratchpadWindow.document.getElementById("sp-menu-revert");
createAndLoadTemporaryFile();
}
function testAfterSaved() {
// Check if the revert menu is disabled as the file is at saved state.
ok(menu.hasAttribute("disabled"), "The revert menu entry is disabled.");
// chancging the text in the file
gScratchpad.setText("\nfoo();", gLength, gLength);
// Checking the text got changed
is(gScratchpad.getText(), gFileContent + "\nfoo();",
"The text changed the first time.");
// Revert menu now should be enabled.
ok(!menu.hasAttribute("disabled"),
"The revert menu entry is enabled after changing text first time");
// reverting back to last saved state.
gScratchpad.revertFile(testAfterRevert);
}
function testAfterRevert() {
// Check if the file's text got reverted
is(gScratchpad.getText(), gFileContent,
"The text reverted back to original text.");
// The revert menu should be disabled again.
ok(menu.hasAttribute("disabled"),
"The revert menu entry is disabled after reverting.");
// chancging the text in the file again
gScratchpad.setText("\nalert(foo.toSource());", gLength, gLength);
// Saving the file.
gScratchpad.saveFile(testAfterSecondSave);
}
function testAfterSecondSave() {
// revert menu entry should be disabled.
ok(menu.hasAttribute("disabled"),
"The revert menu entry is disabled after saving.");
// changing the text.
gScratchpad.setText("\nfoo();", gLength + 23, gLength + 23);
// revert menu entry should get enabled yet again.
ok(!menu.hasAttribute("disabled"),
"The revert menu entry is enabled after changing text third time");
// reverting back to last saved state.
gScratchpad.revertFile(testAfterSecondRevert);
}
function testAfterSecondRevert() {
// Check if the file's text got reverted
is(gScratchpad.getText(), gFileContent + "\nalert(foo.toSource());",
"The text reverted back to the changed saved text.");
// The revert menu should be disabled again.
ok(menu.hasAttribute("disabled"),
"Revert menu entry is disabled after reverting to changed saved state.");
gFile.remove(false);
gFile = null;
gScratchpad = null;
}
function createAndLoadTemporaryFile()
{
// Create a temporary file.
gFile = FileUtils.getFile("TmpD", [gFileName]);
gFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
// Write the temporary file.
let fout = Cc["@mozilla.org/network/file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
fout.init(gFile.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
0644, fout.DEFER_OPEN);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let fileContentStream = converter.convertToInputStream(gFileContent);
NetUtil.asyncCopy(fileContentStream, fout, tempFileSaved);
}
function tempFileSaved(aStatus)
{
ok(Components.isSuccessCode(aStatus),
"the temporary file was saved successfully");
// Import the file into Scratchpad.
gScratchpad.setFilename(gFile.path);
gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true,
testAfterSaved);
}
function test()
{
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
openScratchpad(startTest);
}, true);
content.location = "data:text/html,<p>test reverting to last saved state of" +
" a file </p>";
}

View File

@ -15,6 +15,8 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource:///modules/devtools/Commands.jsm");
const Node = Components.interfaces.nsIDOMNode;
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
@ -137,6 +139,28 @@ DeveloperToolbar.prototype.focus = function DT_focus()
}
};
/**
* Called from browser.xul in response to menu-click or keyboard shortcut to
* toggle the toolbar
*/
DeveloperToolbar.prototype.focusToggle = function DT_focusToggle()
{
if (this.visible) {
// If we have focus then the active element is the HTML input contained
// inside the xul input element
var active = this._chromeWindow.document.activeElement;
var position = this._input.compareDocumentPosition(active);
if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
this.hide();
}
else {
this._input.focus();
}
} else {
this.show(true);
}
};
/**
* Even if the user has not clicked on 'Got it' in the intro, we only show it
* once per session.

View File

@ -758,6 +758,87 @@ StyleEditor.prototype = {
}
},
/**
* Decode a CSS source string to unicode according to the character set rules
* defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
*
* @param string aString
* Source of a CSS stylesheet, loaded from file or cache.
* @param string aChannelCharset
* Charset of the source string if set by the HTTP channel.
* @return string
* The CSS string, in unicode.
*/
_decodeCSSCharset: function SE__decodeCSSCharset(aString, aChannelCharset)
{
// StyleSheet's charset can be specified from multiple sources
if (aChannelCharset.length > 0) {
// step 1 of syndata.html: charset given in HTTP header.
return this._convertToUnicode(aString, aChannelCharset);
}
let sheet = this.styleSheet;
if (sheet) {
// Do we have a @charset rule in the stylesheet?
// step 2 of syndata.html (without the BOM check).
if (sheet.cssRules) {
let rules = sheet.cssRules;
if (rules.length
&& rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
return this._convertToUnicode(aString, rules.item(0).encoding);
}
}
if (sheet.ownerNode) {
// step 3: see <link charset="…">
let linkCharset = sheet.ownerNode.getAttribute("charset");
if (linkCharset != null) {
return this._convertToUnicode(aString, linkCharset);
}
}
// step 4 (1 of 2): charset of referring stylesheet.
let parentSheet = sheet.parentStyleSheet;
if (parentSheet && parentSheet.cssRules &&
parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
return this._convertToUnicode(aString,
parentSheet.cssRules[0].encoding);
}
// step 4 (2 of 2): charset of referring document.
if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
return this._convertToUnicode(aString,
sheet.ownerNode.ownerDocument.characterSet);
}
}
// step 5: default to utf-8.
return this._convertToUnicode(aString, "UTF-8");
},
/**
* Convert a given string, encoded in a given character set, to unicode.
* @param string aString
* A string.
* @param string aCharset
* A character set.
* @return string
* A unicode string.
*/
_convertToUnicode: function SE__convertToUnicode(aString, aCharset) {
// Decoding primitives.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
try {
converter.charset = aCharset;
return converter.ConvertToUnicode(aString);
} catch(e) {
return aString;
}
},
/**
* Load source from a file or file-like resource.
*
@ -790,6 +871,7 @@ StyleEditor.prototype = {
{
let channel = Services.io.newChannel(aHref, null, null);
let chunks = [];
let channelCharset = "";
let streamListener = { // nsIStreamListener inherits nsIRequestObserver
onStartRequest: function (aRequest, aContext, aStatusCode) {
if (!Components.isSuccessCode(aStatusCode)) {
@ -797,6 +879,10 @@ StyleEditor.prototype = {
}
}.bind(this),
onDataAvailable: function (aRequest, aContext, aStream, aOffset, aCount) {
let channel = aRequest.QueryInterface(Ci.nsIChannel);
if (!channelCharset) {
channelCharset = channel.contentCharset;
}
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function (aRequest, aContext, aStatusCode) {
@ -804,7 +890,7 @@ StyleEditor.prototype = {
return this._signalError(LOAD_ERROR);
}
this._onSourceLoad(chunks.join(""));
this._onSourceLoad(chunks.join(""), channelCharset);
}.bind(this)
};
@ -816,9 +902,14 @@ StyleEditor.prototype = {
* Called when source has been loaded.
*
* @param string aSourceText
* @param string aCharset
* Optional. The character set to use. The default is to detect the
* character set following the standard (see
* <http://www.w3.org/TR/CSS2/syndata.html#charset>).
*/
_onSourceLoad: function SE__onSourceLoad(aSourceText)
_onSourceLoad: function SE__onSourceLoad(aSourceText, aCharset)
{
aSourceText = this._decodeCSSCharset(aSourceText, aCharset || "");
this._restoreExpando();
this._state.text = prettifyCSS(aSourceText);
this._loaded = true;

View File

@ -71,6 +71,10 @@ function testEditorAdded(aChrome, aEditor)
function testFirstStyleSheetEditor(aChrome, aEditor)
{
// Note: the html <link> contains charset="UTF-8".
ok(aEditor._state.text.indexOf("\u263a") >= 0,
"stylesheet is unicode-aware.");
//testing TESTCASE's simple.css stylesheet
is(aEditor.styleSheetIndex, 0,
"first stylesheet is at index 0");

View File

@ -1,6 +1,7 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* ☺ */
body {
margin: 0;

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>simple testcase</title>
<link rel="stylesheet" type="text/css" media="screen" href="simple.css"/>
<link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
<style type="text/css">
body {
background: white;

View File

@ -36,6 +36,9 @@
<!ENTITY openRecentMenu.label "Open Recent">
<!ENTITY openRecentMenu.accesskey "R">
<!ENTITY revertCmd.label "Revert…">
<!ENTITY revertCmd.accesskey "t">
<!ENTITY saveFileCmd.label "Save">
<!ENTITY saveFileCmd.accesskey "S">
<!ENTITY saveFileCmd.commandkey "s">

View File

@ -54,6 +54,14 @@ confirmClose=Do you want to save the changes you made to this scratchpad?
# you try to close a scratchpad with unsaved changes.
confirmClose.title=Unsaved Changes
# LOCALIZATION NOTE (confirmRevert): This is message in the prompt dialog when
# you try to revert unsaved content of scratchpad.
confirmRevert=Do you want to revert the changes you made to this scratchpad?
# LOCALIZATION NOTE (confirmRevert.title): This is title of the prompt dialog when
# you try to revert unsaved content of scratchpad.
confirmRevert.title=Revert Changes
# LOCALIZATION NOTE (scratchpadIntro): This is a multi-line comment explaining
# how to use the Scratchpad. Note that this should be a valid JavaScript
# comment inside /* and */.

View File

@ -3156,6 +3156,14 @@ html|*#gcli-output-frame {
0 1px 0 hsla(210,16%,76%,.15);
}
.gclitoolbar-input-node::-moz-selection {
background-color: green !important;
}
.gclitoolbar-input-node {
background-color: red;
}
.gclitoolbar-complete-node {
padding-left: 21px;
background-color: transparent;

View File

@ -3118,6 +3118,10 @@ html|*#gcli-output-frame {
0 0 0 1px hsla(210,40%,83%,.1);
}
.gclitoolbar-input-node {
background-color: green !important;
}
.gclitoolbar-complete-node {
padding-left: 21px;
background-color: transparent;