Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Issue 959: Enable auto-completion of built-in members

  • Loading branch information...
commit cd315da08fd7d717f35778b22f96cb82cc682985 1 parent df926d8
@simonlindholm simonlindholm authored
View
404 extension/content/firebug/console/autoCompleter.js
@@ -4,7 +4,6 @@ define([
"firebug/lib/object",
"firebug/firebug",
"firebug/lib/domplate",
- "firebug/chrome/reps",
"firebug/lib/locale",
"firebug/lib/events",
"firebug/lib/wrapper",
@@ -13,14 +12,11 @@ define([
"firebug/lib/array",
"firebug/editor/editor"
],
-function(Obj, Firebug, Domplate, FirebugReps, Locale, Events, Wrapper, Dom, Str, Arr, Editor) {
+function(Obj, Firebug, Domplate, Locale, Events, Wrapper, Dom, Str, Arr, Editor) {
// ********************************************************************************************* //
// Constants
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
const kwActions = ["throw", "return", "in", "instanceof", "delete", "new",
"typeof", "void", "yield"];
const reOpenBracket = /[\[\(\{]/;
@@ -44,7 +40,9 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
this.completionBase = {
pre: null,
expr: null,
- candidates: []
+ forceShowPopup: false,
+ candidates: [],
+ hiddenCandidates: []
};
this.completions = null;
@@ -79,7 +77,9 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
this.completionBase = {
pre: null,
expr: null,
- candidates: []
+ forceShowPopup: false,
+ candidates: [],
+ hiddenCandidates: []
};
this.completions = null;
@@ -94,6 +94,7 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
this.hideForExpression = function()
{
this.completionBase.candidates = [];
+ this.completionBase.hiddenCandidates = [];
this.completions = null;
this.showCompletions(false);
@@ -183,14 +184,17 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
var prevCompletions = this.completions;
// We only need to calculate a new candidate list if the expression has
- // changed (we can ignore this.completionBase.pre since completions do not
+ // changed (we can ignore completionBase.pre since completions do not
// depend upon that).
if (preExpr !== this.completionBase.expr)
{
this.completionBase.expr = preExpr;
- this.completionBase.candidates = autoCompleteEval(context, preExpr, spreExpr,
+ var ev = autoCompleteEval(context, preExpr, spreExpr,
this.options.includeCurrentScope);
prevCompletions = null;
+ this.completionBase.candidates = ev.completions;
+ this.completionBase.hiddenCandidates = ev.hiddenCompletions;
+ this.completionBase.forceShowPopup = false;
}
this.createCompletions(prop, prevCompletions);
@@ -205,9 +209,6 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
*/
this.createCompletions = function(prefix, prevCompletions)
{
- var candidates = this.completionBase.candidates;
- var valid = [], ciValid = [];
-
if (!this.completionBase.expr && !prefix)
{
// Don't complete "".
@@ -215,31 +216,41 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
return;
}
+ var clist = [
+ this.completionBase.candidates,
+ this.completionBase.hiddenCandidates
+ ], cind = 0;
+ var valid = [], ciValid = [];
var lowPrefix = prefix.toLowerCase();
- for (var i = 0; i < candidates.length; ++i)
+ while (ciValid.length === 0 && cind < 2)
{
- // Mark a candidate as matching if it matches the prefix case-
- // insensitively, and shares its upper-case characters.
- var name = candidates[i];
- if (!Str.hasPrefix(name.toLowerCase(), lowPrefix))
- continue;
-
- var fail = false;
- for (var j = 0; j < prefix.length; ++j)
+ var candidates = clist[cind];
+ for (var i = 0; i < candidates.length; ++i)
{
- var ch = prefix.charAt(j);
- if (ch !== ch.toLowerCase() && ch !== name.charAt(j))
+ // Mark a candidate as matching if it matches the prefix case-
+ // insensitively, and shares its upper-case characters.
+ var name = candidates[i];
+ if (!Str.hasPrefix(name.toLowerCase(), lowPrefix))
+ continue;
+
+ var fail = false;
+ for (var j = 0; j < prefix.length; ++j)
{
- fail = true;
- break;
+ var ch = prefix.charAt(j);
+ if (ch !== ch.toLowerCase() && ch !== name.charAt(j))
+ {
+ fail = true;
+ break;
+ }
+ }
+ if (!fail)
+ {
+ ciValid.push(name);
+ if (Str.hasPrefix(name, prefix))
+ valid.push(name);
}
}
- if (!fail)
- {
- ciValid.push(name);
- if (Str.hasPrefix(name, prefix))
- valid.push(name);
- }
+ ++cind;
}
if (ciValid.length > 0)
@@ -250,18 +261,16 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
this.completions = {
list: (hasMatchingCase ? valid : ciValid),
- prefix: prefix
+ prefix: prefix,
+ hidePopup: (cind === 2)
};
this.completions.index = this.pickDefaultCandidate(prevCompletions);
if (hasMatchingCase)
{
var find = valid[this.completions.index];
- this.completions = {
- list: ciValid,
- prefix: prefix,
- index: ciValid.indexOf(find)
- };
+ this.completions.list = ciValid;
+ this.completions.index = ciValid.indexOf(find);
}
}
else
@@ -290,7 +299,7 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
// Special-case certain expressions.
var special = {
- "": ["document", "console", "window", "parseInt", "undefined"],
+ "": ["document", "console", "frames", "window", "parseInt", "undefined"],
"window.": ["console"],
"location.": ["href"],
"document.": ["getElementById", "addEventListener", "createElement",
@@ -312,12 +321,32 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
}
}
+ // 'prototype' is a good default if it exists.
+ ind = list.indexOf("prototype");
+ if (ind !== -1)
+ return ind;
+
ind = 0;
for (var i = 1; i < list.length; ++i)
{
if (list[i].length < list[ind].length)
ind = i;
}
+
+ // Avoid some completions in favor of others.
+ var replacements = {
+ "toSource": "toString",
+ "toFixed": "toString",
+ "watch": "toString",
+ "pattern": "parentNode"
+ };
+ if (replacements.hasOwnProperty(list[ind]))
+ {
+ var ind2 = list.indexOf(replacements[list[ind]]);
+ if (ind2 !== -1)
+ return ind2;
+ }
+
return ind;
};
@@ -378,12 +407,16 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
{
this.completionBox.value = this.getCompletionBoxValue();
- var show = this.showCompletionPopup ||
- (this.completionPopup && this.completionPopup.state === "open");
- if (show && this.completions && this.completions.list.length > 1)
+ if (this.completions && (this.completionBase.forceShowPopup ||
+ (this.completions.list.length > 1 && this.showCompletionPopup &&
+ !this.completions.hidePopup)))
+ {
this.popupCandidates(cycling);
+ }
else
+ {
this.closePopup();
+ }
};
/**
@@ -498,20 +531,24 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
// does not close itself and prevent event propagation on keypress.
// (Unless the popup is only open due to Ctrl+Space, in which case
// that's precisely what we want.)
- if (this.showCompletionPopup)
+ if (!this.forceShowPopup)
this.closePopup();
}
else if (event.keyCode === KeyEvent.DOM_VK_SPACE && Events.isControl(event))
{
- // Force-show the completion popup.
if (!this.completions)
{
// If completions have been hidden, show them again.
this.hide();
this.complete(context);
}
- if (this.completionPopup && this.completions)
+
+ if (this.completions && !this.isPopupOpen())
+ {
+ // Force-show the completion popup.
+ this.completionBase.forceShowPopup = true;
this.popupCandidates(false);
+ }
}
};
@@ -1271,8 +1308,7 @@ function killCompletions(expr, origExpr)
}
// Types the autocompletion knows about, some of their non-enumerable properties,
-// and the return types of some member functions, included in the Firebug.CommandLine
-// object to make it more easily extensible.
+// and the return types of some member functions.
var AutoCompletionKnownTypes = {
"void": {
@@ -1457,9 +1493,8 @@ var AutoCompletionKnownTypes = {
"tan": "|Number"
},
"Number": {
- // There are also toFixed and valueOf, but they are left out because
- // they steal focus from toString by being shorter (in the case of
- // toFixed), and because they are used very seldom.
+ "valueOf": "|Number",
+ "toFixed": "|String",
"toExponential": "|String",
"toPrecision": "|String",
"toLocaleString": "|String",
@@ -1535,77 +1570,173 @@ function getTypeExtractionExpression(command)
return ret;
}
-function propChainBuildComplete(out, context, tempExpr, result)
+/**
+ * Compare two property names a and b with a custom sort order. The comparison
+ * is lexicographical, but treats _ as higher than other letters in the
+ * beginning of the word, so that:
+ * $ < AutoCompleter < add_widget < additive < _ < _priv < __proto__
+ * @return -1, 0 or 1 depending on whether (a < b), (a == b) or (a > b).
+ */
+function comparePropertyNames(lhs, rhs)
{
- var complete = null, command = null;
- if (tempExpr.fake)
+ var len = Math.min(lhs.length, rhs.length);
+ for (var i = 0; i < len; ++i)
{
- var name = tempExpr.value.val;
- complete = getFakeCompleteKeys(name);
- if (!getKnownType(name)._fb_ignorePrototype)
- command = name + ".prototype";
+ var u1 = (lhs.charAt(i) === "_");
+ var u2 = (rhs.charAt(i) === "_");
+ if (!u1 && !u2)
+ break;
+ if (!u1 || !u2)
+ return (u1 ? 1 : -1);
}
- else
+
+ if (lhs < rhs)
+ return -1;
+ return (lhs === rhs ? 0 : 1);
+}
+
+function propertiesToHide(expr, obj)
+{
+ var ret = [];
+
+ // __{define,lookup}[SG]etter__ appear as own properties on lots of DOM objects.
+ ret.push("__defineGetter__", "__defineSetter__",
+ "__lookupGetter__", "__lookupSetter__");
+
+ // function.caller/argument are deprecated and ugly.
+ if (typeof obj === "function")
+ ret.push("caller", "arguments");
+
+ if (Object.prototype.toString.call(obj) === "[object String]")
{
- if (typeof result === "string")
- {
- // Strings only have indices as properties, use the fake object
- // completions instead.
- tempExpr.fake = true;
- tempExpr.value = getKnownTypeInfo("String");
- propChainBuildComplete(out, context, tempExpr);
- return;
- }
- else if (FirebugReps.Arr.isArray(result, context.window))
- complete = nonNumericKeys(result);
- else
- complete = Arr.keys(result);
- command = getTypeExtractionExpression(tempExpr.command);
+ // Unused, cluttery.
+ ret.push("toLocaleLowerCase", "toLocaleUpperCase", "quote", "bold",
+ "italics", "fixed", "fontsize", "fontcolor", "link", "anchor",
+ "strike", "small", "big", "blink", "sup", "sub");
}
- var done = function()
+ // Annoying when typing 'document'/'window'.
+ if (expr === "")
{
- if (out.indexCompletion)
+ ret.push("Document", "DocumentType", "DocumentFragment",
+ "DocumentTouch", "DocumentXBL", "DOMTokenList",
+ "DOMConstructor", "DOMError", "DOMException",
+ "DOMImplementation", "DOMRequest", "DOMSettableTokenList",
+ "DOMStringMap", "DOMStringList", "Window", "WindowInternal",
+ "WindowCollection", "WindowUtils", "WindowPerformance");
+ }
+
+ if (expr === "" || expr === "window.")
+ {
+ // Internal Firefox things.
+ ret.push("getInterface", "Components", "XPCNativeWrapper",
+ "InstallTrigger", "netscape",
+ "startProfiling", "stopProfiling", "pauseProfilers",
+ "resumeProfilers", "dumpProfile");
+
+ // Hide ourselves.
+ ret.push("_FirebugCommandLine", "_firebug");
+ }
+
+ // Old and ugly.
+ if (expr === "document.")
+ ret.push("fgColor", "vlinkColor", "linkColor");
+ if (expr === "document.body.")
+ ret.push("link", "aLink", "vLink");
+
+ // Rather universal and feel like built-ins.
+ ret.push("valueOf", "toSource", "constructor", "QueryInterface");
+
+ return ret;
+}
+
+
+function setCompletionsFromObject(out, object, context)
+{
+ // 'object' is a user-level, non-null object.
+ try
+ {
+ var isObjectPrototype = function(obj)
{
- complete = complete.map(function(x)
+ // Check if an object is "Object.prototype". This isn't as simple
+ // as 'obj === context.window.wrappedJSObject.Object.prototype' due
+ // to cross-window properties, nor just '!Object.getPrototypeOf(obj)'
+ // because of Object.create.
+ return !Object.getPrototypeOf(obj) && "hasOwnProperty" in obj;
+ }
+
+ var obj = object;
+ while (obj !== null)
+ {
+ var target = (isObjectPrototype(obj) ?
+ out.hiddenCompletions : out.completions);
+ target.push.apply(target, Object.getOwnPropertyNames(obj));
+ obj = Object.getPrototypeOf(obj);
+ }
+
+ // As a special case, when completing "Object.prototype." no properties
+ // should be hidden.
+ if (isObjectPrototype(object))
+ {
+ out.completions = out.hiddenCompletions;
+ out.hiddenCompletions = [];
+ }
+ else
+ {
+ // Hide a list of well-chosen annoying properties.
+ var hide = propertiesToHide(out.spreExpr, object);
+ var hideMap = Object.create(null);
+ for (var i = 0; i < hide.length; ++i)
+ hideMap[hide[i]] = 1;
+
+ var newCompletions = [];
+ out.completions.forEach(function(prop)
{
- x = (out.indexQuoteType === '"') ? Str.escapeJS(x): Str.escapeSingleQuoteJS(x);
- return x + out.indexQuoteType + "]";
+ if (prop in hideMap)
+ out.hiddenCompletions.push(prop);
+ else
+ newCompletions.push(prop);
});
+ out.completions = newCompletions;
}
- // Properties may be taken from several sources, so filter out duplicates.
- out.complete = Arr.sortUnique(complete);
- };
-
- if (command === null)
+ // Firefox hides __proto__ - add it back.
+ if ("__proto__" in object)
+ out.hiddenCompletions.push("__proto__");
+ }
+ catch (exc)
{
- done();
+ if (FBTrace.DBG_COMMANDLINE)
+ FBTrace.sysout("autoCompleter.getCompletionsFromPrototypeChain failed", exc);
}
- else
+}
+
+function propChainBuildComplete(out, context, tempExpr, result)
+{
+ var done = function(result)
{
- Firebug.CommandLine.evaluate(command, context, context.thisValue, null,
+ if (result !== undefined && result !== null)
+ setCompletionsFromObject(out, Object(result), context);
+ };
+
+ if (tempExpr.fake)
+ {
+ var name = tempExpr.value.val;
+ if (getKnownType(name)._fb_ignorePrototype)
+ return;
+ var command = name + ".prototype";
+ Firebug.CommandLine.evaluate(name + ".prototype", context, context.thisValue, null,
function found(result, context)
{
- if (tempExpr.fake)
- {
- complete = complete.concat(Arr.keys(result));
- }
- else
- {
- if (typeof result === "string" && getKnownType(result))
- {
- complete = complete.concat(getFakeCompleteKeys(result));
- }
- }
- done();
+ done(result);
},
- function failed(result, context)
- {
- done();
- }
+ function failed(result, context) { }
);
}
+ else
+ {
+ done(result);
+ }
}
function evalPropChainStep(step, tempExpr, evalChain, out, context)
@@ -1896,9 +2027,12 @@ function evalPropChain(out, preExpr, origExpr, context)
function autoCompleteEval(context, preExpr, spreExpr, includeCurrentScope)
{
- var out = {};
-
- out.complete = [];
+ var out = {
+ spreExpr: spreExpr,
+ completions: [],
+ hiddenCompletions: []
+ };
+ var indexCompletion = false;
try
{
@@ -1908,11 +2042,10 @@ function autoCompleteEval(context, preExpr, spreExpr, includeCurrentScope)
// In case of array indexing, remove the bracket and set a flag to
// escape completions.
- out.indexCompletion = false;
var len = spreExpr.length;
if (len >= 2 && spreExpr[len-2] === "[" && spreExpr[len-1] === '"')
{
- out.indexCompletion = true;
+ indexCompletion = true;
out.indexQuoteType = preExpr[len-1];
spreExpr = spreExpr.substr(0, len-2);
preExpr = preExpr.substr(0, len-2);
@@ -1934,7 +2067,7 @@ function autoCompleteEval(context, preExpr, spreExpr, includeCurrentScope)
// Don't auto-complete '.'.
if (spreExpr === "")
- return out.complete;
+ return out;
evalPropChain(out, spreExpr, preExpr, context);
}
@@ -1945,32 +2078,53 @@ function autoCompleteEval(context, preExpr, spreExpr, includeCurrentScope)
var contentView = Wrapper.getContentView(context.window);
if (context.stopped && includeCurrentScope)
{
- out.complete = Firebug.Debugger.getCurrentFrameKeys(context);
+ out.completions = Firebug.Debugger.getCurrentFrameKeys(context);
}
else if (contentView && contentView.Window &&
contentView.constructor.toString() === contentView.Window.toString())
// Cross window type pseudo-comparison
{
- out.complete = Arr.keys(contentView); // return is safe
-
- // Add some known window properties
- out.complete = out.complete.concat(getFakeCompleteKeys("Window"));
+ setCompletionsFromObject(out, contentView, context);
}
else // hopefully sandbox in Chromebug
{
- out.complete = Arr.keys(context.global);
+ setCompletionsFromObject(out, context.global, context);
+ }
+ }
+
+ // Add "] to properties if we are doing index-completions.
+ if (indexCompletion)
+ {
+ function convertQuotes(x)
+ {
+ x = (out.indexQuoteType === '"') ? Str.escapeJS(x): Str.escapeSingleQuoteJS(x);
+ return x + out.indexQuoteType + "]";
}
+ out.completions = out.completions.map(convertQuotes);
+ out.hiddenCompletions = out.hiddenCompletions.map(convertQuotes);
+ }
- // Sort the completions, and avoid duplicates.
- out.complete = Arr.sortUnique(out.complete);
+ // Remove numeric keys.
+ var rePositiveNumber = /^[1-9][0-9]*$/;
+ var nonNumeric = function(x)
+ {
+ return x !== '0' && !rePositiveNumber.test(x);
}
+ out.completions = out.completions.filter(nonNumeric);
+ out.hiddenCompletions = out.hiddenCompletions.filter(nonNumeric);
+
+ // Sort the completions, and avoid duplicates.
+ // XXX: If we make it possible to show both regular and hidden completions
+ // at the same time, completions must shadow hiddenCompletions.
+ out.completions = Arr.sortUnique(out.completions, comparePropertyNames);
+ out.hiddenCompletions = Arr.sortUnique(out.hiddenCompletions, comparePropertyNames);
}
catch (exc)
{
if (FBTrace.DBG_ERRORS && FBTrace.DBG_COMMANDLINE)
FBTrace.sysout("commandLine.autoCompleteEval FAILED", exc);
}
- return out.complete;
+ return out;
}
var reValidJSToken = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
@@ -1988,26 +2142,6 @@ function isValidProperty(value)
return reValidJSToken.test(value);
}
-const rePositiveNumber = /^[1-9][0-9]*$/;
-function nonNumericKeys(map) // keys will be on user-level window objects
-{
- var keys = [];
- try
- {
- for (var name in map) // enumeration is safe
- {
- if (! (name === "0" || rePositiveNumber.test(name)) )
- keys.push(name);
- }
- }
- catch (exc)
- {
- // Sometimes we get exceptions trying to iterate properties
- }
-
- return keys; // return is safe
-}
-
function setCursorToEOL(input)
{
// textbox version, https://developer.mozilla.org/en/XUL/Property/inputField
View
2  extension/content/firebug/js/debugger.js
@@ -124,7 +124,7 @@ Firebug.Debugger = Obj.extend(Firebug.ActivableModule,
/**
* Used by autocomplete in commandLine
- * @return array of global property names
+ * @return array of locally visible property names for each scope we are in
*/
getCurrentFrameKeys: function(context) // TODO remote, on bti
{
Please sign in to comment.
Something went wrong with that request. Please try again.