1
0
mirror of https://github.com/adobe/brackets.git synced 2024-11-20 18:02:54 +01:00

Merge pull request #3439 from adobe-research/master

Switch js code hinting to be powered by tern.
This commit is contained in:
Kevin Dangoor 2013-04-18 11:19:38 -07:00
commit 372ce5af1f
20 changed files with 1304 additions and 6091 deletions

6
.gitmodules vendored
View File

@ -10,3 +10,9 @@
[submodule "src/thirdparty/mustache"]
path = src/thirdparty/mustache
url = https://github.com/janl/mustache.js.git
[submodule "src/extensions/default/JavaScriptCodeHints/thirdparty/tern"]
path = src/extensions/default/JavaScriptCodeHints/thirdparty/tern
url = https://github.com/marijnh/tern.git
[submodule "src/extensions/default/JavaScriptCodeHints/thirdparty/acorn"]
path = src/extensions/default/JavaScriptCodeHints/thirdparty/acorn
url = https://github.com/marijnh/acorn.git

View File

@ -27,10 +27,15 @@
define(function (require, exports, module) {
"use strict";
var LANGUAGE_ID = "javascript",
SCOPE_MSG_TYPE = "outerScope",
SINGLE_QUOTE = "\'",
DOUBLE_QUOTE = "\"";
var LANGUAGE_ID = "javascript",
SINGLE_QUOTE = "'",
DOUBLE_QUOTE = "\"",
TERN_INIT_MSG = "Init",
TERN_JUMPTODEF_MSG = "JumptoDef",
TERN_COMPLETIONS_MSG = "Completions",
TERN_GET_FILE_MSG = "GetFile",
TERN_GET_PROPERTIES_MSG = "Properties",
TERN_CALLED_FUNC_TYPE_MSG = "FunctionType";
/**
* Create a hint token with name value that occurs at the given list of
@ -57,9 +62,7 @@ define(function (require, exports, module) {
* @return {boolean} - could key be a valid identifier?
*/
function maybeIdentifier(key) {
return (/[0-9a-z_.\$]/i).test(key) ||
(key.indexOf(SINGLE_QUOTE) === 0) ||
(key.indexOf(DOUBLE_QUOTE) === 0);
return (/[0-9a-z_\$]/i).test(key);
}
/**
@ -78,7 +81,18 @@ define(function (require, exports, module) {
return true;
}
}
/**
* Determine if hints should be displayed for the given key.
*
* @param {string} key - key entered by the user
* @return {boolean} true if the hints should be shown for the key,
* false otherwise.
*/
function hintableKey(key) {
return (key === null || key === "." || maybeIdentifier(key));
}
/**
* Divide a path into directory and filename parts
*
@ -89,7 +103,7 @@ define(function (require, exports, module) {
function splitPath(path) {
var index = path.lastIndexOf("/"),
dir = path.substring(0, index),
file = path.substring(index, path.length);
file = path.substring(index + 1, path.length);
return {dir: dir, file: file };
}
@ -106,93 +120,36 @@ define(function (require, exports, module) {
return name + "." + EVENT_TAG;
}
/*
* Annotate list of identifiers with their scope level.
*
* @param {Array.<Object>} identifiers - list of identifier tokens to be
* annotated
* @param {Scope} scope - scope object used to determine the scope level of
* each identifier token in the previous list.
* @return {Array.<Object>} - the input array; to each object in the array a
* new level {number} property has been added to indicate its scope
* level.
*/
function annotateWithScope(identifiers, scope) {
return identifiers.map(function (t) {
var level = scope.contains(t.value);
if (level >= 0) {
t.level = level;
} else {
t.level = -1;
}
return t;
});
}
/*
* Annotate a list of properties with their association level
*
* @param {Array.<Object>} properties - list of property tokens
* @param {Association} association - an object that maps property
* names to the number of times it occurs in a particular context
* @return {Array.<Object>} - the input array; to each object in the array a
* new level {number} property has been added to indicate the number
* of times the property has occurred in the association context.
*/
function annotateWithAssociation(properties, association) {
return properties.map(function (t) {
if (association[t.value] > 0) {
t.level = 0;
}
return t;
});
}
/*
* Annotate a list of tokens as being global variables
*
* @param {Array.<Object>} globals - list of identifier tokens
* @return {Array.<Object>} - the input array; to each object in the array a
* new global {boolean} property has been added to indicate that it is
* a global variable.
*/
function annotateGlobals(globals) {
return globals.map(function (t) {
t.global = true;
return t;
});
}
/*
* Annotate a list of tokens as literals of a particular kind;
* if string literals, annotate with an appropriate delimiter.
*
* if string literals, annotate with an appropriate delimiter.
*
* @param {Array.<Object>} literals - list of hint tokens
* @param {string} kind - the kind of literals in the list (e.g., "string")
* @return {Array.<Object>} - the input array; to each object in the array a
* new literal {boolean} property has been added to indicate that it
* is a literal hint, and also a new kind {string} property to indicate
* the literal kind. For string literals, a delimiter property is also
* added to indicate what the default delimiter should be (viz. a
* added to indicate what the default delimiter should be (viz. a
* single or double quotation mark).
*/
function annotateLiterals(literals, kind) {
return literals.map(function (t) {
t.literal = true;
t.kind = kind;
t.origin = "ecma5";
if (kind === "string") {
if (/[\\\\]*[^\\]"/.test(t.value)) {
t.delimeter = SINGLE_QUOTE;
t.delimiter = SINGLE_QUOTE;
} else {
t.delimeter = DOUBLE_QUOTE;
t.delimiter = DOUBLE_QUOTE;
}
}
return t;
});
}
/*
/*
* Annotate a list of tokens as keywords
*
* @param {Array.<Object>} keyword - list of keyword tokens
@ -203,31 +160,11 @@ define(function (require, exports, module) {
function annotateKeywords(keywords) {
return keywords.map(function (t) {
t.keyword = true;
t.origin = "ecma5";
return t;
});
}
/*
* Annotate a list of tokens with a path name
*
* @param {Array.<Object>} tokens - list of hint tokens
* @param {string} dir - directory with which to annotate the hints
* @param {string} file - file name with which to annotate the hints
* @return {Array.<Object>} - the input array; to each object in the array a
* new path {string} property has been added, equal to dir + file.
*/
function annotateWithPath(tokens, dir, file) {
var path = dir + file;
return tokens.map(function (t) {
t.path = path;
return t;
});
}
// TODO 1: The definitions below should be defined in a separate JSON file.
// TODO 2: Add properties and associations for the builtin globals.
var KEYWORD_NAMES = [
"break", "case", "catch", "continue", "debugger", "default", "delete",
"do", "else", "finally", "for", "function", "if", "in", "instanceof",
@ -247,141 +184,22 @@ define(function (require, exports, module) {
}),
LITERALS = annotateLiterals(LITERAL_TOKENS);
var JSL_GLOBAL_NAMES = [
"clearInterval", "clearTimeout", "document", "event", "frames",
"history", "Image", "location", "name", "navigator", "Option",
"parent", "screen", "setInterval", "setTimeout", "window",
"XMLHttpRequest", "alert", "confirm", "console", "Debug", "opera",
"prompt", "WSH", "Buffer", "exports", "global", "module", "process",
"querystring", "require", "__filename", "__dirname", "defineClass",
"deserialize", "gc", "help", "load", "loadClass", "print", "quit",
"readFile", "readUrl", "runCommand", "seal", "serialize", "spawn",
"sync", "toint32", "version", "ActiveXObject", "CScript", "Enumerator",
"System", "VBArray", "WScript"
],
JSL_GLOBAL_TOKENS = annotateGlobals(JSL_GLOBAL_NAMES.map(function (t) {
return makeToken(t, []);
})),
JSL_GLOBALS = JSL_GLOBAL_TOKENS.reduce(function (prev, curr) {
prev[curr.value] = curr;
return prev;
}, {}); // builds an object from the array of tokens.
// Predefined sets of globals as defined by JSLint
var JSL_GLOBALS_BROWSER = [
JSL_GLOBALS.clearInterval,
JSL_GLOBALS.clearTimeout,
JSL_GLOBALS.document,
JSL_GLOBALS.event,
JSL_GLOBALS.frames,
JSL_GLOBALS.history,
JSL_GLOBALS.Image,
JSL_GLOBALS.location,
JSL_GLOBALS.name,
JSL_GLOBALS.navigator,
JSL_GLOBALS.Option,
JSL_GLOBALS.parent,
JSL_GLOBALS.screen,
JSL_GLOBALS.setInterval,
JSL_GLOBALS.setTimeout,
JSL_GLOBALS.window,
JSL_GLOBALS.XMLHttpRequest
],
JSL_GLOBALS_DEVEL = [
JSL_GLOBALS.alert,
JSL_GLOBALS.confirm,
JSL_GLOBALS.console,
JSL_GLOBALS.Debug,
JSL_GLOBALS.opera,
JSL_GLOBALS.prompt,
JSL_GLOBALS.WSH
],
JSL_GLOBALS_NODE = [
JSL_GLOBALS.Buffer,
JSL_GLOBALS.clearInterval,
JSL_GLOBALS.clearTimeout,
JSL_GLOBALS.console,
JSL_GLOBALS.exports,
JSL_GLOBALS.global,
JSL_GLOBALS.module,
JSL_GLOBALS.process,
JSL_GLOBALS.querystring,
JSL_GLOBALS.require,
JSL_GLOBALS.setInterval,
JSL_GLOBALS.setTimeout,
JSL_GLOBALS.__filename,
JSL_GLOBALS.__dirname
],
JSL_GLOBALS_RHINO = [
JSL_GLOBALS.defineClass,
JSL_GLOBALS.deserialize,
JSL_GLOBALS.gc,
JSL_GLOBALS.help,
JSL_GLOBALS.load,
JSL_GLOBALS.loadClass,
JSL_GLOBALS.print,
JSL_GLOBALS.quit,
JSL_GLOBALS.readFile,
JSL_GLOBALS.readUrl,
JSL_GLOBALS.runCommand,
JSL_GLOBALS.seal,
JSL_GLOBALS.serialize,
JSL_GLOBALS.spawn,
JSL_GLOBALS.sync,
JSL_GLOBALS.toint32,
JSL_GLOBALS.version
],
JSL_GLOBALS_WINDOWS = [
JSL_GLOBALS.ActiveXObject,
JSL_GLOBALS.CScript,
JSL_GLOBALS.Debug,
JSL_GLOBALS.Enumerator,
JSL_GLOBALS.System,
JSL_GLOBALS.VBArray,
JSL_GLOBALS.WScript,
JSL_GLOBALS.WSH
];
var JSL_GLOBAL_DEFS = {
browser : JSL_GLOBALS_BROWSER,
devel : JSL_GLOBALS_DEVEL,
node : JSL_GLOBALS_NODE,
rhino : JSL_GLOBALS_RHINO,
windows : JSL_GLOBALS_WINDOWS
};
var BUILTIN_GLOBAL_NAMES = [
"Array", "Boolean", "Date", "Function", "Iterator", "Number", "Object",
"RegExp", "String", "ArrayBuffer", "DataView", "Float32Array",
"Float64Array", "Int16Array", "Int32Array", "Int8Array", "Uint16Array",
"Uint32Array", "Uint8Array", "Uint8ClampedArray", "Error", "EvalError",
"InternalError", "RangeError", "ReferenceError", "StopIteration",
"SyntaxError", "TypeError", "URIError", "decodeURI",
"decodeURIComponent", "encodeURI", "encodeURIComponent", "eval",
"isFinite", "isNaN", "parseFloat", "parseInt", "uneval", "Infinity",
"JSON", "Math", "NaN"
],
BUILTIN_GLOBAL_TOKENS = BUILTIN_GLOBAL_NAMES.map(function (t) {
return makeToken(t, []);
}),
BUILTIN_GLOBALS = annotateGlobals(BUILTIN_GLOBAL_TOKENS);
exports.makeToken = makeToken;
exports.hintable = hintable;
exports.hintableKey = hintableKey;
exports.maybeIdentifier = maybeIdentifier;
exports.splitPath = splitPath;
exports.eventName = eventName;
exports.annotateWithPath = annotateWithPath;
exports.annotateLiterals = annotateLiterals;
exports.annotateGlobals = annotateGlobals;
exports.annotateWithScope = annotateWithScope;
exports.annotateWithAssociation = annotateWithAssociation;
exports.JSL_GLOBAL_DEFS = JSL_GLOBAL_DEFS;
exports.BUILTIN_GLOBALS = BUILTIN_GLOBALS;
exports.KEYWORDS = KEYWORDS;
exports.LITERALS = LITERALS;
exports.LANGUAGE_ID = LANGUAGE_ID;
exports.SCOPE_MSG_TYPE = SCOPE_MSG_TYPE;
exports.SINGLE_QUOTE = SINGLE_QUOTE;
exports.DOUBLE_QUOTE = DOUBLE_QUOTE;
exports.TERN_JUMPTODEF_MSG = TERN_JUMPTODEF_MSG;
exports.TERN_COMPLETIONS_MSG = TERN_COMPLETIONS_MSG;
exports.TERN_INIT_MSG = TERN_INIT_MSG;
exports.TERN_GET_FILE_MSG = TERN_GET_FILE_MSG;
exports.TERN_GET_PROPERTIES_MSG = TERN_GET_PROPERTIES_MSG;
exports.TERN_CALLED_FUNC_TYPE_MSG = TERN_CALLED_FUNC_TYPE_MSG;
});

View File

@ -1,682 +0,0 @@
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define */
define(function (require, exports, module) {
"use strict";
/*
* Performs a binary search among an array of scope objects for one with a
* range that contains pos. The array must be non-empty and sorted
* ascending according to the objects' (disjoint) ranges.
*
* @param {Array.<Object>} arr - the sorted array of scope objects to
* search
* @param {number} pos - the position to search for in arr
* @return {Object} - the scope object containing pos
*/
function binaryRangeSearch(arr, pos) {
var low = 0,
high = arr.length,
middle = Math.floor(high / 2);
// binary search for the position among the sorted ranges
while (low < middle && middle < high) {
if (arr[middle].range.end < pos) {
low = middle;
middle += Math.floor((high - middle) / 2);
} else if (arr[middle].range.start > pos) {
high = middle;
middle = low + Math.floor((middle - low) / 2);
} else {
break;
}
}
return arr[middle];
}
/**
* Build a scope object from AST tree as a child of the given parent.
*
* @constructor
* @param {AST} tree - an AST as described at
https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API
* @param {?Scope=} parent - the (optional) parent of the new Scope
*/
function Scope(tree, parent) {
/*
* Given a member expression, try to add a target-property association
* to the given scope.
*
* @param {AST} object - the lookup object
* @param {AST} property - the property being looked up
* @param {Scope} parent - the Scope object in which the association
* occurs.
*/
function _buildAssociations(object, property, parent) {
if (property.type === "Identifier") {
if (object.type === "Identifier") {
parent.addAssociation(object, property);
} else if (object.type === "MemberExpression") {
if (object.computed === false) {
_buildAssociations(object.property, property, parent);
}
} else if (object.type === "CallExpression") {
_buildAssociations(object.callee, property, parent);
} else if (object.type === "ThisExpression") {
object.name = "this";
parent.addAssociation(object, property);
} else {
// most likely a literal
return;
}
} else {
// Because we restrict to non-computed property lookups, this
// should be unreachable
throw "Expected identifier but found " + property.type;
}
}
/*
* Walk a given AST and add scope information to a given parent scope,
* including new child Scope objects.
*
* @param {AST} tree - the AST from which the Scope is constructed
* @param {Scope} parent - the parent of the Scope to be constructed
*/
function _buildScope(tree, parent) {
var child;
if (tree === undefined || tree === null) {
return;
}
switch (tree.type) {
case "Program":
case "BlockStatement":
tree.body.forEach(function (t) {
_buildScope(t, parent);
});
break;
case "FunctionDeclaration":
parent.addDeclaration(tree.id);
_buildScope(tree.id, parent);
child = new Scope(tree, parent);
child.addAllDeclarations(tree.params);
tree.params.forEach(function (t) {
_buildScope(t, child);
});
parent.addChildScope(child);
_buildScope(tree.body, child);
break;
case "VariableDeclaration":
// FIXME handle let scoping
tree.declarations.forEach(function (t) {
_buildScope(t, parent);
});
break;
case "VariableDeclarator":
parent.addDeclaration(tree.id);
_buildScope(tree.id, parent);
if (tree.init !== null) {
_buildScope(tree.init, parent);
}
break;
case "ExpressionStatement":
_buildScope(tree.expression, parent);
break;
case "SwitchStatement":
_buildScope(tree.discriminant, parent);
if (tree.cases) {
tree.cases.forEach(function (t) {
_buildScope(t, parent);
});
}
break;
case "SwitchCase":
tree.consequent.forEach(function (t) {
_buildScope(t, parent);
});
if (tree.test) {
_buildScope(tree.test, parent);
}
break;
case "TryStatement":
tree.handlers.forEach(function (t) {
_buildScope(t, parent);
});
_buildScope(tree.block, parent);
if (tree.finalizer) {
_buildScope(tree.finalizer, parent);
}
break;
case "ThrowStatement":
_buildScope(tree.argument, parent);
break;
case "WithStatement":
_buildScope(tree.object, parent);
_buildScope(tree.body, parent);
break;
case "CatchClause":
if (tree.guard) {
_buildScope(tree.guard, parent);
}
// FIXME: Is this the correct way to handle catch?
child = new Scope(tree, parent);
child.addDeclaration(tree.param);
_buildScope(tree.param, child);
parent.addChildScope(child);
_buildScope(tree.body, child);
break;
case "ReturnStatement":
if (tree.argument) {
_buildScope(tree.argument, parent);
}
break;
case "ForStatement":
_buildScope(tree.body, parent);
if (tree.init) {
_buildScope(tree.init, parent);
}
if (tree.test) {
_buildScope(tree.test, parent);
}
if (tree.update) {
_buildScope(tree.update, parent);
}
break;
case "ForInStatement":
_buildScope(tree.left, parent);
_buildScope(tree.right, parent);
_buildScope(tree.body, parent);
break;
case "LabeledStatement":
_buildScope(tree.body, parent);
break;
case "BreakStatement":
case "ContinueStatement":
if (tree.label) {
_buildScope(tree.label, parent);
}
break;
case "UpdateExpression":
case "UnaryExpression":
_buildScope(tree.argument, parent);
break;
case "IfStatement":
case "ConditionalExpression":
_buildScope(tree.test, parent);
_buildScope(tree.consequent, parent);
if (tree.alternate) {
_buildScope(tree.alternate, parent);
}
break;
case "WhileStatement":
case "DoWhileStatement":
_buildScope(tree.test, parent);
_buildScope(tree.body, parent);
break;
case "SequenceExpression":
tree.expressions.forEach(function (t) {
_buildScope(t, parent);
});
break;
case "ObjectExpression":
tree.properties.forEach(function (t) {
_buildScope(t, parent);
});
break;
case "ArrayExpression":
tree.elements.forEach(function (t) {
_buildScope(t, parent);
});
break;
case "NewExpression":
if (tree["arguments"]) { // pacifies JSLint
tree["arguments"].forEach(function (t) {
_buildScope(t, parent);
});
}
_buildScope(tree.callee, parent);
break;
case "BinaryExpression":
case "AssignmentExpression":
case "LogicalExpression":
_buildScope(tree.left, parent);
_buildScope(tree.right, parent);
break;
case "MemberExpression":
_buildScope(tree.object, parent);
_buildScope(tree.property, parent);
if (tree.property && tree.property.type === "Identifier") {
parent.addProperty(tree.property);
}
if (tree.computed === false) {
_buildAssociations(tree.object, tree.property, parent);
}
break;
case "CallExpression":
tree["arguments"].forEach(function (t) {
_buildScope(t, parent);
});
_buildScope(tree.callee, parent);
break;
case "FunctionExpression":
if (tree.id) {
parent.addDeclaration(tree.id);
_buildScope(tree.id, parent);
}
child = new Scope(tree, parent);
parent.addChildScope(child);
child.addAllDeclarations(tree.params);
tree.params.forEach(function (t) {
_buildScope(t, child);
});
_buildScope(tree.body, child);
break;
case "Property":
// Undocumented or Esprima-specific?
parent.addProperty(tree.key);
_buildScope(tree.value, parent);
break;
case "Identifier":
parent.addIdOccurrence(tree);
break;
case "Literal":
if (tree.value && typeof tree.value === "string") {
parent.addLiteralOccurrence(tree);
}
break;
case "DebuggerStatement":
case "EmptyStatement":
case "ThisExpression":
break;
default:
throw "Unknown node type: " + tree.type;
}
}
if (parent === undefined) {
this.parent = null;
} else {
this.parent = parent;
}
this.idDeclarations = {};
this.idOccurrences = [];
this.propOccurrences = [];
this.associations = [];
this.literals = [];
this.children = []; // disjoint ranges, ordered by range start
this.range = {
start: tree.range[0],
end: tree.range[1]
};
// if parent is null, walk the AST
if (!this.parent) {
_buildScope(tree, this);
}
}
/*
* Rebuild a Scope object from an object that has all the necessary data
* but the wrong prototype. Such objects may be created as a result of
* e.g., JSON-marshalling and unmarshalling a scope.
*
* @param {Object} data - an object that contains all data of a Scope object
* but none of the methods
* @return {Scope} - the same object with Scope methods added
*/
Scope.rebuild = function (data) {
var memberName,
member;
for (memberName in Scope.prototype) {
if (Scope.prototype.hasOwnProperty(memberName)) {
member = Scope.prototype[memberName];
if (typeof member === "function") {
data[memberName] = member;
}
}
}
data.children.forEach(function (child) {
Scope.rebuild(child);
});
return data;
};
/*
* Add an identifier occurrence.
*
* @param {AST} id - an identifier AST node
*/
Scope.prototype.addIdOccurrence = function (id) {
this.idOccurrences.push(id);
};
/*
* Add an identifier declaration
*
* @param {AST} id - an identifier AST node
*/
Scope.prototype.addDeclaration = function (id) {
this.idDeclarations[id.name] = id;
this.addIdOccurrence(id);
};
/*
* Add a list of identifier declarations
*
* @param {Array.<AST>} ids - a list of identifier AST nodes
*/
Scope.prototype.addAllDeclarations = function (ids) {
var that = this;
ids.forEach(function (i) {
that.addDeclaration(i);
});
};
/*
* Add a property occurrence
*
* @param {AST} prop - a property AST node
*/
Scope.prototype.addProperty = function (prop) {
this.propOccurrences.push(prop);
};
/*
* Add an association object
*
* @param {AST} obj - an identifier AST node
* @param {AST} prop - a property AST node
*/
Scope.prototype.addAssociation = function (obj, prop) {
this.associations.push({object: obj, property: prop});
};
/*
* Add a literal occurrence
*
* @param {AST} lit - a literal AST node
*/
Scope.prototype.addLiteralOccurrence = function (lit) {
this.literals.push(lit);
};
/*
* Attach a new child scope to the current scope. Inserts the child scope
* in range-sorted order w.r.t. the other children of the current scope.
*
* @param {Scope} child - the child to be added
*/
Scope.prototype.addChildScope = function (child) {
var i = 0;
while (i < this.children.length &&
child.range.start > this.children[i].range.end) {
i++;
}
this.children.splice(i, 0, child);
};
/*
* Is the symbol declared in this scope?
*
* @param {string} sym - a symbol name
* @return {boolean} - whether a symbol with that name is declared in this
* immediate scope
*/
Scope.prototype.member = function (sym) {
return Object.prototype.hasOwnProperty.call(this.idDeclarations, sym);
};
/*
* Is the symbol declared in this scope or a parent scope?
*
* @param {string} sym - a symbol name
* @return {boolean} - whether a symbol with that name is declared in this
* immediate scope or a parent scope
*/
Scope.prototype.contains = function (sym) {
var depth = 0,
child = this;
do {
if (child.member(sym)) {
return depth;
} else {
child = child.parent;
depth++;
}
} while (child !== null);
return undefined;
};
/*
* Does this scope, or its children, contain the given position?
*
* @param {number} pos - the position to test for inclusion in the scope
* @return {boolean} - is the position included in the scope?
*/
Scope.prototype.containsPosition = function (pos) {
return this.range.start <= pos && pos <= this.range.end;
};
/*
* Does this scope, but not its children, contain the given position?
*
* @param {number} pos - the position to test for inclusion in the scope
* @return {boolean} - is the position directly included in the scope?
*/
Scope.prototype.containsPositionImmediate = function (pos) {
if (this.containsPosition(pos)) {
if (this.children.length === 0) {
// contains the position and there are no children
return true;
} else {
var child = binaryRangeSearch(this.children, pos);
// contains the position if the nearest child does not
return !child.containsPosition(pos);
}
} else {
// does not contain the position
return false;
}
};
/*
* Find the child scope of the current scope for a given position
*
* @param {number} pos - the position at which to find a child scope
* @return {?Scope} - the child scope for the given position, or null
* if none exists
*/
Scope.prototype.findChild = function (pos) {
if (this.containsPosition(pos)) {
if (this.children.length === 0) {
// there are no children, so this is the most precise scope
return this;
} else {
var child = binaryRangeSearch(this.children, pos);
// the most precise scope is the most precise scope of the child,
// unless no child contains the position, in which case this is
// the most precise scope
return child.findChild(pos) || this;
}
} else {
return null;
}
};
/**
* Traverse the scope down via children in pre-order.
*
* @param {Function} add - the Scope accumulation function
* @param {Object} init - an initial value for the accumulation function
* @return {Object} - the result of accumulating the current scope along
* with all of its children
*/
Scope.prototype.walkDown = function (add, init) {
var result = add(this, init);
this.children.forEach(function (child) {
result = child.walkDown(add, result);
});
return result;
};
/*
* Traverse a particular list in the scope down via children
*
* @param {Function} addItem - the item accumulation function
* @param {Object} init - an initial value for the accumulation function
* @param {string} listName - the name of a Scope property
* @return {Object} - the result of accumulating the given property for
* the current scope along with all of its children
*/
Scope.prototype.walkDownList = function (addItem, init, listName) {
function addList(scope, init) {
var list = scope[listName];
return list.reduce(function (prev, curr) {
return addItem(prev, curr);
}, init);
}
return this.walkDown(addList, init);
};
/*
* Traverse identifier occurrences in the scope down via children
*
* @param {Function} add - the identifier accumulation function
* @param {Object} init - an initial value for the accumulation function
* @return {Object} - the result of accumulating identifier occurrences
* for the current scope along with all of its children
*/
Scope.prototype.walkDownIdentifiers = function (add, init) {
return this.walkDownList(add, init, "idOccurrences");
};
/*
* Traverse property occurrences in the scope down via children
*
* @param {Function} add - the property accumulation function
* @param {Object} init - an initial value for the accumulation function
* @return {Object} - the result of of accumulating property occurrences
* for the current scope along with all of its children
*/
Scope.prototype.walkDownProperties = function (add, init) {
return this.walkDownList(add, init, "propOccurrences");
};
/*
* Traverse associations in the scope down via children
*
* @param {Function} add - the association accumulation function
* @param {Object} init - an initial value for the accumulation function
* @return {Object} - the result of of accumulating association occurrences
* for the current scope along with all of its children
*/
Scope.prototype.walkDownAssociations = function (add, init) {
return this.walkDownList(add, init, "associations");
};
/*
* Traverse literals in the scope down via children
*
* @param {Function} add - the literal accumulation function
* @param {Object} init - an initial value for the accumulation function
* @return {Object} - the result of of accumulating literal occurrences
* for the current scope along with all of its children
*/
Scope.prototype.walkDownLiterals = function (add, init) {
return this.walkDownList(add, init, "literals");
};
/**
* Traverse the scope up via the parent
*
* @param {Function} add - the Scope accumulation function
* @param {Object} init - an initial value for the accumulation function
* @param {string} prop - the property name to combine scope information for
* @return {Object} - the result of of accumulating the current scope along
* with its parents
*/
Scope.prototype.walkUp = function (add, init, prop) {
var scope = this,
result = init,
combine = function (elem) {
result = add(result, elem);
};
while (scope !== null) {
this[prop].forEach(combine);
scope = scope.parent;
}
return result;
};
module.exports = Scope;
});

View File

@ -36,332 +36,220 @@ define(function (require, exports, module) {
var DocumentManager = brackets.getModule("document/DocumentManager"),
LanguageManager = brackets.getModule("language/LanguageManager"),
FileUtils = brackets.getModule("file/FileUtils"),
NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem,
ProjectManager = brackets.getModule("project/ProjectManager"),
HintUtils = require("HintUtils"),
Scope = require("Scope");
var pendingRequest = null, // information about a deferred scope request
fileState = {}, // directory -> file -> state
outerScopeWorker = (function () {
CollectionUtils = brackets.getModule("utils/CollectionUtils"),
HintUtils = require("HintUtils");
var ternEnvironment = [],
pendingTernRequests = {},
builtinFiles = ["ecma5.json", "browser.json", "jquery.json"],
builtinLibraryNames = [],
rootTernDir = null,
projectRoot = null,
ternPromise = null,
resolvedFiles = {}, // file -> resolved file
_ternWorker = (function () {
var path = module.uri.substring(0, module.uri.lastIndexOf("/") + 1);
return new Worker(path + "parser-worker.js");
return new Worker(path + "tern-worker.js");
}());
var MAX_TEXT_LENGTH = 1000000, // about 1MB
MAX_FILES_IN_DIR = 100;
/**
* Initialize state for a given directory and file name
*
* @param {string} dir - the directory name to initialize
* @param {string} file - the file name to initialize
/**
* Create a new tern server.
*/
function initFileState(dir, file) {
// initialize outerScope, etc. at dir
if (!fileState.hasOwnProperty(dir)) {
fileState[dir] = {};
}
if (file !== undefined) {
if (!fileState[dir].hasOwnProperty(file)) {
fileState[dir][file] = {
// global scope object for this file
scope : null,
// has the file changed since the scope was updated?
dirtyFile : true,
// has the scope changed since the last inner scope request?
dirtyScope : true,
// is the parser worker active for this file?
active : false,
// all variable and parameter names defined in this file
identifiers : null,
// all property names found in this file
properties : null,
// all globals defined in this file
globals : null,
// all string literals found in this file
literals : null,
// all context-property associations found in this file
associations : null
};
}
}
function initTernServer(dir, files) {
_ternWorker.postMessage({
type : HintUtils.TERN_INIT_MSG,
dir : dir,
files : files,
env : ternEnvironment
});
rootTernDir = dir + "/";
}
/**
* Get the file state for a given path. If just the directory is given
* instead of the whole path, a set of file states is returned, one for
* each (known) file in the directory.
*
* @param {string} dir - the directory name for which state is desired
* @param {string=} file - the file name for which state is desired
* @return {Object} - a file state object (as documented within
* intializeFileState above), or a set of file state objects if
* file is omitted.
* An array of library names that contain JavaScript builtins definitions.
*
* @returns {Array} - array of library names.
*/
function getFileState(dir, file) {
initFileState(dir, file);
if (file === undefined) {
return fileState[dir];
} else {
return fileState[dir][file];
}
function getBuiltins() {
return builtinLibraryNames;
}
/**
* Request a new outer scope object from the parser worker, if necessary
*
* @param {string} dir - the directory name for which the outer scope is to
* be refreshed
* @param {string} file - the file name for which the outer scope is to be
* refreshed
* @param {string} text - the text of the file for which the outer scope is
* to be refreshed
* Read in the json files that have type information for the builtins, dom,etc
*/
function refreshOuterScope(dir, file, text) {
function initTernEnv() {
var path = module.uri.substring(0, module.uri.lastIndexOf("/") + 1) + "thirdparty/tern/defs/",
files = builtinFiles,
library;
if (text.length > MAX_TEXT_LENGTH) {
return;
}
var state = getFileState(dir, file);
// if there is not yet an outer scope or if the file has changed then
// we might need to update the outer scope
if (state.scope === null || state.dirtyFile) {
if (!state.active) {
var path = dir + file,
entry = new NativeFileSystem.FileEntry(path);
// the outer scope worker is about to be active
state.active = true;
// the file will be clean since the last outer scope request
state.dirtyFile = false;
// send text to the parser worker
outerScopeWorker.postMessage({
type : HintUtils.SCOPE_MSG_TYPE,
dir : dir,
file : file,
text : text,
force : !state.scope
});
}
}
}
/**
* Get inner scope information for a given file and offset if a suitable
* global scope object is availble; otherwise, return a promise for such
* information, resolved when a suitable global scope object becomes
* available.
*
* @param {string} dir - the directory name for which the inner scope is to
* be refreshed
* @param {string} file - the file name for which the inner scope is to be
* refreshed
* @param {number} offset - offset into the text at which the inner scope
* is to be refreshed
* @return {Object + jQuery.Promise} - inner scope information, or a promise
* for such information, including the local scope object and lists of
* identifiers, properties, globals, literals and associations.
*/
function refreshInnerScope(dir, file, offset) {
/*
* Filter a list of tokens using a given scope object
*
* @param {Array.<Object>} tokens - a list of identifier tokens
* @param {Scope} scope - a scope object
* @return {Array.<Object>} - the sublist of the input list that
* contains all and only the identifier tokens in scope
* w.r.t. to the given scope
*/
function filterByScope(tokens, scope) {
return tokens.filter(function (id) {
var level = scope.contains(id.value);
return (level >= 0);
files.forEach(function (i) {
DocumentManager.getDocumentForPath(path + i).done(function (document) {
library = JSON.parse(document.getText());
builtinLibraryNames.push(library["!name"]);
ternEnvironment.push(library);
}).fail(function (error) {
console.log("failed to read tern config file " + i);
});
}
});
}
initTernEnv();
/**
* Send a message to the tern worker - if the worker is being initialized,
* the message will not be posted until initialization is complete
*/
function postMessage(msg) {
ternPromise.done(function (ternWorker) {
ternWorker.postMessage(msg);
});
}
/**
* Get a Promise for the definition from TernJS, for the file & offset passed in.
* @return {jQuery.Promise} - a promise that will resolve to definition when
* it is done
*/
function getJumptoDef(dir, file, offset, text) {
postMessage({
type: HintUtils.TERN_JUMPTODEF_MSG,
dir: dir,
file: file,
offset: offset,
text: text
});
var $deferredJump = $.Deferred();
pendingTernRequests[file] = $deferredJump;
return $deferredJump.promise();
}
/**
* Request Jump-To-Definition from Tern.
*
* @param {session} session - the session
* @param {Document} document - the document
* @param {number} offset - the offset into the document
* @return {jQuery.Promise} - The promise will not complete until tern
* has completed.
*/
function requestJumptoDef(session, document, offset) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
file = split.file;
/*
* Combine a particular property from a set of sets using a given add
* operation
*
* @param {Object} sets - a set of sets
* @param {string} propName - the property to pick out from each set
* @param {Function} add - the function that combines properties from
* each set
* @return {Object}- the result of combining each set's property using
* the add function
*/
function merge(sets, propName, add) {
var combinedSet = {},
nextSet,
file;
for (file in sets) {
if (sets.hasOwnProperty(file)) {
nextSet = sets[file][propName];
if (nextSet) {
add(combinedSet, nextSet);
}
}
}
return combinedSet;
}
/*
* Combine properties from files in the current file's directory into
* a single list.
*
* @param {string} dir - the directory name of the files for which
* property lists should be merged
* @param {Array.<Object>} - the combined list of property tokens
*/
function mergeProperties(dir) {
function addPropObjs(obj1, obj2) {
function addToObj(obj, token) {
if (!Object.prototype.hasOwnProperty.call(obj, token.value)) {
obj[token.value] = token;
}
}
obj2.forEach(function (token) {
addToObj(obj1, token);
});
}
var stateMap = getFileState(dir),
propObj = merge(stateMap, "properties", addPropObjs),
propList = [],
propName;
for (propName in propObj) {
if (Object.prototype.hasOwnProperty.call(propObj, propName)) {
propList.push(propObj[propName]);
}
}
return propList;
}
/*
* Combine association set objects from all of the files in a given
* directory
*
* @param {string} dir - the directory name of the files for which
* association sets should be merged
* @param {Object} - the combined association set object
*/
function mergeAssociations(dir) {
function addAssocSets(list1, list2) {
var name;
function addAssocObjs(assoc1, assoc2) {
var name;
for (name in assoc2) {
if (Object.prototype.hasOwnProperty.call(assoc2, name)) {
if (Object.prototype.hasOwnProperty.call(assoc1, name)) {
assoc1[name] = assoc1[name] + assoc2[name];
} else {
assoc1[name] = assoc2[name];
}
}
}
}
for (name in list2) {
if (Object.prototype.hasOwnProperty.call(list2, name)) {
if (Object.prototype.hasOwnProperty.call(list1, name)) {
addAssocObjs(list1[name], list2[name]);
} else {
list1[name] = list2[name];
}
}
}
}
var stateMap = getFileState(dir);
return merge(stateMap, "associations", addAssocSets);
}
var state = getFileState(dir, file);
var ternPromise = getJumptoDef(dir, file, offset, document.getText());
// If there is no outer scope, the inner scope request is deferred.
if (!state.scope) {
if (pendingRequest && pendingRequest.deferred.state() === "pending") {
pendingRequest.deferred.reject();
}
return {promise: ternPromise};
}
pendingRequest = {
dir : dir,
file : file,
offset : offset,
deferred : $.Deferred()
};
// Request the outer scope from the parser worker.
DocumentManager.getDocumentForPath(dir + file).done(function (document) {
refreshOuterScope(dir, file, document.getText());
});
return { promise: pendingRequest.deferred.promise() };
/**
* Handle the response from the tern web worker when
* it responds with the definition
*
* @param response - the response from the worker
*/
function handleJumptoDef(response) {
var file = response.file;
var $deferredJump = pendingTernRequests[file];
pendingTernRequests[file] = null;
if ($deferredJump) {
$deferredJump.resolveWith(null, [response]);
}
}
/**
* Add a pending request waiting for the tern-worker to complete.
*
* @param {string} file - the name of the file
* @param {string} type - the type of request
* @param {jQuery.Deferred} deferredRequest - the $.Deferred object to save
*/
function addPendingRequest(file, offset, type) {
var requests,
key = file + "@" + offset,
$deferredRequest;
if (Object.prototype.hasOwnProperty.call(pendingTernRequests, key)) {
requests = pendingTernRequests[key];
} else {
// The inner scope will be clean after this
state.dirtyScope = false;
// Try to find an inner scope from the current outer scope
var innerScope = state.scope.findChild(offset);
if (!innerScope) {
// we may have failed to find a child scope because a
// character was added to the end of the file, outside of
// the (now out-of-date and currently-being-updated)
// outer scope. Hence, if offset is greater than the range
// of the outerScope, we manually set innerScope to the
// outerScope
innerScope = state.scope;
}
// FIXME: This could be more efficient if instead of filtering
// the entire list of identifiers we just used the identifiers
// in the scope of innerScope, but that list doesn't have the
// accumulated position information.
var identifiersForScope = filterByScope(state.identifiers, innerScope),
propertiesForFile = mergeProperties(dir),
associationsForFile = mergeAssociations(dir);
return {
scope : innerScope,
identifiers : identifiersForScope,
globals : state.globals,
literals : state.literals,
properties : propertiesForFile,
associations: associationsForFile
};
requests = {};
pendingTernRequests[key] = requests;
}
if (Object.prototype.hasOwnProperty.call(requests, type)) {
$deferredRequest = requests[type];
} else {
requests[type] = $deferredRequest = $.Deferred();
}
return $deferredRequest.promise();
}
/**
* Get a new inner scope and related info, if possible. If there is no
* outer scope for the given file, a promise will be returned instead.
* (See refreshInnerScope above.)
* Get a Promise for the completions from TernJS, for the file & offset passed in.
* @return {jQuery.Promise} - a promise that will resolve to an array of completions when
* it is done
*/
function getTernHints(dir, file, offset, text) {
postMessage({
type: HintUtils.TERN_COMPLETIONS_MSG,
dir: dir,
file: file,
offset: offset,
text: text
});
return addPendingRequest(file, offset, HintUtils.TERN_COMPLETIONS_MSG);
}
/**
* Get a Promise for all of the known properties from TernJS, for the directory and file.
* The properties will be used as guesses in tern.
*
* @return {jQuery.Promise} - a promise that will resolve to an array of properties when
* it is done
*/
function getTernProperties(dir, file, offset, text) {
postMessage({
type: HintUtils.TERN_GET_PROPERTIES_MSG,
dir: dir,
file: file,
offset: offset,
text: text
});
return addPendingRequest(file, offset, HintUtils.TERN_GET_PROPERTIES_MSG);
}
/**
* Get a Promise for the function type from TernJS.
* @return {jQuery.Promise} - a promise that will resolve to the function type of the function being called.
*/
function getTernFunctionType(dir, file, pos, offset, text) {
postMessage({
type: HintUtils.TERN_CALLED_FUNC_TYPE_MSG,
dir: dir,
file: file,
pos: pos,
offset: offset,
text: text
});
return addPendingRequest(file, offset, HintUtils.TERN_CALLED_FUNC_TYPE_MSG);
}
/**
* Request hints from Tern.
*
* Note that successive calls to getScope may return the same objects, so
* clients that wish to modify those objects (e.g., by annotating them based
@ -372,49 +260,146 @@ define(function (require, exports, module) {
* desired
* @param {number} offset - the offset into the document at which scope
* info is desired
* @return {Object + jQuery.Promise} - the inner scope info, or a promise
* for such info. (See refreshInnerScope above.)
* @return {jQuery.Promise} - The promise will not complete until the tern
* hints have completed.
*/
function getScopeInfo(document, offset) {
function requestHints(session, document, offset) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
file = split.file;
return refreshInnerScope(dir, file, offset);
}
/**
* Is the inner scope dirty? (It is if the outer scope has changed since
* the last inner scope request)
*
* @param {Document} document - the document for which the last requested
* inner scope may or may not be dirty
* @return {boolean} - is the inner scope dirty?
*/
function isScopeDirty(document) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
file = split.file,
state = getFileState(dir, file);
var $deferredHints = $.Deferred(),
hintPromise,
fnTypePromise,
propsPromise;
return state.dirtyScope;
hintPromise = getTernHints(dir, file, offset, document.getText());
var sessionType = session.getType();
if (sessionType.property) {
propsPromise = getTernProperties(dir, file, offset, document.getText());
} else {
var $propsDeferred = $.Deferred();
propsPromise = $propsDeferred.promise();
$propsDeferred.resolveWith(null);
}
if (sessionType.showFunctionType) {
// Show function sig
fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, document.getText());
} else {
var $fnTypeDeferred = $.Deferred();
fnTypePromise = $fnTypeDeferred.promise();
$fnTypeDeferred.resolveWith(null);
}
$.when(hintPromise, fnTypePromise, propsPromise).done(
function (completions, fnType, properties) {
session.setTernHints(completions);
session.setFnType(fnType);
session.setTernProperties(properties);
$deferredHints.resolveWith(null);
}
);
return {promise: $deferredHints.promise()};
}
/**
* Mark a file as dirty, which may cause a later outer scope request to
* trigger a reparse request.
*
* @param {string} dir - the directory name of the file to be marked dirty
* @param {string} file - the file name of the file to be marked dirty
* Get any pending $.Deferred object waiting on the specified file and request type
* @param {string} file - the file
* @param {string} type - the type of request
* @param {jQuery.Deferred} - the $.Deferred for the request
*/
function markFileDirty(dir, file) {
var state = getFileState(dir, file);
function getPendingRequest(file, offset, type) {
var key = file + "@" + offset;
if (CollectionUtils.hasProperty(pendingTernRequests, key)) {
var requests = pendingTernRequests[key],
requestType = requests[type];
state.dirtyFile = true;
delete pendingTernRequests[key][type];
if (!Object.keys(requests).length) {
delete pendingTernRequests[key];
}
return requestType;
}
}
/**
* Handle the response from the tern web worker when
* it responds with the list of completions
*
* @param {{dir:string, file:string, offset:number, completions:Array.<string>}} response - the response from the worker
*/
function handleTernCompletions(response) {
var file = response.file,
offset = response.offset,
completions = response.completions,
properties = response.properties,
fnType = response.fnType,
type = response.type,
$deferredHints = getPendingRequest(file, offset, type);
if ($deferredHints) {
if (completions) {
$deferredHints.resolveWith(null, [completions]);
} else if (properties) {
$deferredHints.resolveWith(null, [properties]);
} else if (fnType) {
$deferredHints.resolveWith(null, [fnType]);
}
}
}
/**
* @param {string} file a relative path
* @return {string} returns the path we resolved when we tried to parse the file, or undefined
*/
function getResolvedPath(file) {
return resolvedFiles[file];
}
/**
* Handle a request from the worker for text of a file
*
* @param {{file:string}} request - the request from the worker. Should be an Object containing the name
* of the file tern wants the contents of
*/
function handleTernGetFile(request) {
function replyWith(name, txt) {
postMessage({
type: HintUtils.TERN_GET_FILE_MSG,
file: name,
text: txt
});
}
var name = request.file;
DocumentManager.getDocumentForPath(rootTernDir + name).done(function (document) {
resolvedFiles[name] = rootTernDir + name;
replyWith(name, document.getText());
})
.fail(function () {
if (projectRoot) {
// Try relative to project root
DocumentManager.getDocumentForPath(projectRoot + name).done(function (document) {
resolvedFiles[name] = projectRoot + name;
replyWith(name, document.getText());
})
.fail(function () {
replyWith(name, "");
});
} else {
// Need to send something back to tern - it will wait
// until all the files have been retrieved before doing its calculations
replyWith(name, "");
}
});
}
/**
* Called each time a new editor becomes active. Refreshes the outer scopes
* of the given file as well as of the other files in the given directory.
@ -425,13 +410,18 @@ define(function (require, exports, module) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
files = [],
file = split.file;
var ternDeferred = $.Deferred();
ternPromise = ternDeferred.promise();
pendingTernRequests = [];
resolvedFiles = {};
projectRoot = ProjectManager.getProjectRoot() ? ProjectManager.getProjectRoot().fullPath : null;
NativeFileSystem.resolveNativeFileSystemPath(dir, function (dirEntry) {
var reader = dirEntry.createReader();
markFileDirty(dir, file);
reader.readEntries(function (entries) {
entries.slice(0, MAX_FILES_IN_DIR).forEach(function (entry) {
if (entry.isFile) {
@ -443,135 +433,55 @@ define(function (require, exports, module) {
if (file.indexOf(".") > 1) { // ignore /.dotfiles
var languageID = LanguageManager.getLanguageForPath(entry.fullPath).getId();
if (languageID === HintUtils.LANGUAGE_ID) {
DocumentManager.getDocumentForPath(path).done(function (document) {
refreshOuterScope(dir, file, document.getText());
});
files.push(file);
}
}
}
});
initTernServer(dir, files);
ternDeferred.resolveWith(null, [_ternWorker]);
}, function (err) {
console.log("Unable to refresh directory: " + err);
refreshOuterScope(dir, file, document.getText());
});
}, function (err) {
console.log("Directory \"%s\" does not exist", dir);
});
}
/*
* Called each time the file associated with the active edtor changes.
* Called each time the file associated with the active editor changes.
* Marks the file as being dirty and refresh its outer scope.
*
* @param {Document} document - the document that has changed
*/
function handleFileChange(document) {
var path = document.file.fullPath,
split = HintUtils.splitPath(path),
dir = split.dir,
file = split.file;
markFileDirty(dir, file);
refreshOuterScope(dir, file, document.getText());
}
/*
* Receive an outer scope object from the parser worker and resolves any
* deferred inner scope requests.
*
* @param {Object} response - the parser response object, which includes
* the global scope and lists of identifiers, properties, globals,
* literals and associations.
*/
function handleOuterScope(response) {
var dir = response.dir,
file = response.file,
path = dir + file,
state = getFileState(dir, file),
scopeInfo;
if (state.active) {
state.active = false;
if (response.success) {
state.scope = Scope.rebuild(response.scope);
// The outer scope should cover the entire file
state.scope.range.start = 0;
state.scope.range.end = response.length;
state.globals = response.globals;
state.identifiers = response.identifiers;
state.properties = response.properties;
state.literals = response.literals;
state.associations = response.associations;
state.dirtyScope = true;
if (state.dirtyFile) {
DocumentManager.getDocumentForPath(path).done(function (document) {
refreshOuterScope(dir, file, document.getText());
});
}
if (pendingRequest !== null && pendingRequest.dir === dir &&
pendingRequest.file === file) {
if (pendingRequest.deferred.state() === "pending") {
scopeInfo = refreshInnerScope(dir, file, pendingRequest.offset);
if (scopeInfo && !scopeInfo.deferred) {
pendingRequest.deferred.resolveWith(null, [scopeInfo]);
pendingRequest = null;
}
}
}
}
} else {
console.log("Expired scope request: " + path);
}
}
// handle response objects, otherwise log the message
outerScopeWorker.addEventListener("message", function (e) {
_ternWorker.addEventListener("message", function (e) {
var response = e.data,
type = response.type;
if (type === HintUtils.SCOPE_MSG_TYPE) {
handleOuterScope(response);
if (type === HintUtils.TERN_COMPLETIONS_MSG ||
type === HintUtils.TERN_CALLED_FUNC_TYPE_MSG ||
type === HintUtils.TERN_GET_PROPERTIES_MSG) {
// handle any completions the worker calculated
handleTernCompletions(response);
} else if (type === HintUtils.TERN_GET_FILE_MSG) {
// handle a request for the contents of a file
handleTernGetFile(response);
} else if (type === HintUtils.TERN_JUMPTODEF_MSG) {
handleJumptoDef(response);
} else {
console.log("Worker: " + (response.log || response));
}
});
// reset state on project change
$(ProjectManager)
.on(HintUtils.eventName("beforeProjectClose"),
function (event, projectRoot) {
fileState = {};
});
// relocate scope information on file rename
$(DocumentManager)
.on(HintUtils.eventName("fileNameChange"),
function (event, oldName, newName) {
var oldSplit = HintUtils.splitPath(oldName),
oldDir = oldSplit.dir,
oldFile = oldSplit.file,
newSplit = HintUtils.splitPath(newName),
newDir = newSplit.dir,
newFile = newSplit.file;
if (fileState.hasOwnProperty(oldDir) &&
fileState[oldDir].hasOwnProperty(oldFile)) {
if (!fileState.hasOwnProperty(newDir)) {
fileState[newDir] = {};
}
fileState[newDir][newFile] = fileState[oldDir][oldFile];
delete fileState[oldDir][oldFile];
}
});
exports.getBuiltins = getBuiltins;
exports.handleEditorChange = handleEditorChange;
exports.handleFileChange = handleFileChange;
exports.getScopeInfo = getScopeInfo;
exports.isScopeDirty = isScopeDirty;
exports.requestJumptoDef = requestJumptoDef;
exports.requestHints = requestHints;
exports.getTernHints = getTernHints;
exports.getResolvedPath = getResolvedPath;
});

View File

@ -27,7 +27,8 @@
define(function (require, exports, module) {
"use strict";
var HintUtils = require("HintUtils"),
var StringMatch = brackets.getModule("utils/StringMatch"),
HintUtils = require("HintUtils"),
ScopeManager = require("ScopeManager");
/**
@ -40,24 +41,13 @@ define(function (require, exports, module) {
function Session(editor) {
this.editor = editor;
this.path = editor.document.file.fullPath;
this.ternHints = [];
this.ternProperties = [];
this.fnType = null;
this.builtins = ScopeManager.getBuiltins();
this.builtins.push("requirejs.js"); // consider these globals as well.
}
/**
* Update the scope information assocated with the current session
*
* @param {Object} scopeInfo - scope information, including the scope and
* lists of identifiers, globals, literals and properties, and a set
* of associations
*/
Session.prototype.setScopeInfo = function (scopeInfo) {
this.scope = scopeInfo.scope;
this.identifiers = scopeInfo.identifiers;
this.globals = scopeInfo.globals;
this.literals = scopeInfo.literals;
this.properties = scopeInfo.properties;
this.associations = scopeInfo.associations;
};
/**
* Get the name of the file associated with the current session
*
@ -76,6 +66,17 @@ define(function (require, exports, module) {
Session.prototype.getCursor = function () {
return this.editor.getCursorPos();
};
/**
* Get the text of a line.
*
* @param {number} line - the line number
* @return {string} - the text of the line
*/
Session.prototype.getLine = function (line) {
var doc = this.editor.document;
return doc.getLine(line);
};
/**
* Get the offset of the current cursor position
@ -172,14 +173,16 @@ define(function (require, exports, module) {
query = "";
if (token) {
if (token.string !== ".") {
// If the token string is not an identifier, then the query string
// is empty.
if (HintUtils.maybeIdentifier(token.string)) {
query = token.string.substring(0, token.string.length - (token.end - cursor.ch));
query = query.trim();
}
}
return query;
};
/**
* Find the context of a property lookup. For example, for a lookup
* foo(bar, baz(quux)).prop, foo is the context.
@ -217,20 +220,52 @@ define(function (require, exports, module) {
* Get the type of the current session, i.e., whether it is a property
* lookup and, if so, what the context of the lookup is.
*
* @return {{property: boolean, context: string}} - a pair consisting
* @return {{property: boolean,
showFunctionType:boolean,
context: string,
functionCallPos: {line:number, ch:number}}} - an Object consisting
* of a {boolean} "property" that indicates whether or not the type of
* the session is a property lookup, and a {string} "context" that
* indicates the object context (as described in getContext above) of
* the property lookup, or null if there is none. The context is
* always null for non-property lookups.
* a {boolean} "showFunctionType" indicating if the function type should
* be displayed instead of normal hints. If "showFunctionType" is true, then
* then "functionCallPos" will be an object with line & col information of the
* function being called
*/
Session.prototype.getType = function () {
var propertyLookup = false,
context = null,
cursor = this.getCursor(),
token = this.getToken(cursor);
var propertyLookup = false,
inFunctionCall = false,
showFunctionType = false,
context = null,
cursor = this.getCursor(),
functionCallPos,
token = this.getToken(cursor);
if (token) {
if (token.state.lexical.info === "call") {
inFunctionCall = true;
if (this.getQuery().length > 0) {
inFunctionCall = false;
showFunctionType = false;
} else {
showFunctionType = true;
var col = token.state.lexical.column,
line,
e,
found;
for (line = this.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) {
if (this.getLine(line).charAt(col) === "(") {
found = true;
break;
}
}
if (found) {
functionCallPos = {line: line, ch: col};
}
}
}
if (token.string === ".") {
propertyLookup = true;
context = this.getContext(cursor);
@ -245,10 +280,14 @@ define(function (require, exports, module) {
context = this.getContext(cursor);
}
}
if (propertyLookup) { showFunctionType = false; }
}
return {
property: propertyLookup,
inFunctionCall: inFunctionCall,
showFunctionType: showFunctionType,
functionCallPos: functionCallPos,
context: context
};
};
@ -256,309 +295,125 @@ define(function (require, exports, module) {
/**
* Get a list of hints for the current session using the current scope
* information.
*
*
* @param {string} query - the query prefix
* @param {StringMatcher} matcher - the class to find query matches and sort the results
* @return {Array.<Object>} - the sorted list of hints for the current
* session.
*/
Session.prototype.getHints = function () {
/*
* Comparator for sorting tokens according to minimum distance from
* a given position
*
* @param {number} pos - the position to which a token's occurrences
* are compared
* @param {Function} - the comparator function
*/
function compareByPosition(pos) {
/*
* Compute the minimum distance between a token, with which is
* associated a sorted list of positions, and a given offset.
*
* @param {number} pos - the position to which a token's occurrences
* are compared.
* @param {Object} token - a hint token, annotated with a list of
* occurrence positions
* @return number - the least distance of an occurrence of token to
* pos, or Infinity if there are no occurrences
*/
function minDistToPos(pos, token) {
var arr = token.positions,
low = 0,
high = arr.length,
middle = Math.floor(high / 2),
dist;
Session.prototype.getHints = function (query, matcher) {
if (high === 0) {
return Infinity;
} else {
// binary search for the position
while (low < middle && middle < high) {
if (arr[middle] < pos) {
low = middle;
middle += Math.floor((high - middle) / 2);
} else if (arr[middle] > pos) {
high = middle;
middle = low + Math.floor((middle - low) / 2);
} else {
break;
}
}
// the closest position is off by no more than one
dist = Math.abs(arr[middle] - pos);
if (middle > 0) {
dist = Math.min(dist, Math.abs(arr[middle - 1] - pos));
}
if (middle + 1 < arr.length) {
dist = Math.min(dist, Math.abs(arr[middle + 1] - pos));
}
return dist;
}
}
return function (a, b) {
/*
* Look up the cached minimum distance from an occurrence of
* the token to the given position, calculating and storing it
* if needed.
*
* @param {Object} token - the token from which the minimum
* distance to position pos is required
* @return {number} - the least distance of an occurrence of
* token to position pos
*/
function getDistToPos(token) {
var dist;
if (token.distToPos >= 0) {
dist = token.distToPos;
} else {
dist = minDistToPos(pos, token);
token.distToPos = dist;
}
return dist;
}
var aDist = getDistToPos(a),
bDist = getDistToPos(b);
if (aDist === Infinity) {
if (bDist === Infinity) {
return 0;
} else {
return 1;
}
} else {
if (bDist === Infinity) {
return -1;
} else {
return aDist - bDist;
}
}
};
if (query === undefined) {
query = "";
}
/*
* Comparator for sorting tokens lexicographically according to scope,
* assuming the scope level has already been annotated.
*
* @param {Object} a - a token
* @param {Object} b - another token
* @param {number} - comparator value that indicates whether a is more
* tightly scoped than b
*/
function compareByScope(a, b) {
var adepth = a.level;
var bdepth = b.level;
if (adepth >= 0) {
if (bdepth >= 0) {
return adepth - bdepth;
} else {
return -1;
}
} else {
if (bdepth >= 0) {
return 1;
} else {
return 0;
}
}
}
/*
* Comparator for sorting tokens by name
*
* @param {Object} a - a token
* @param {Object} b - another token
* @return {number} - comparator value that indicates whether the name
* of token a is lexicographically lower than the name of token b
*/
function compareByName(a, b) {
if (a.value === b.value) {
return 0;
} else if (a.value < b.value) {
return -1;
} else {
return 1;
}
}
/*
* Comparator for sorting tokens by path, such that a <= b if
* a.path === path
*
* @param {string} path - the target path name
* @return {Function} - the comparator function
*/
function compareByPath(path) {
return function (a, b) {
if (a.path === path) {
if (b.path === path) {
return 0;
} else {
return -1;
}
} else {
if (b.path === path) {
return 1;
} else {
return 0;
}
}
};
}
/*
* Comparator for sorting properties w.r.t. an association object.
*
* @param {Object} assoc - an association object
* @return {Function} - the comparator function
*/
function compareByAssociation(assoc) {
return function (a, b) {
if (Object.prototype.hasOwnProperty.call(assoc, a.value)) {
if (Object.prototype.hasOwnProperty.call(assoc, b.value)) {
return assoc[a.value] - assoc[b.value];
} else {
return -1;
}
} else {
if (Object.prototype.hasOwnProperty.call(assoc, b.value)) {
return 1;
} else {
return 0;
}
}
};
}
/*
* Forms the lexicographical composition of comparators, i.e.,
* "a lex(c1,c2) b" iff "a c1 b or (a = b and a c2 b)"
*
* @param {Function} compare1 - a comparator
* @param {Function} compare2 - another comparator
* @return {Function} - the lexicographic composition of comparator1
* and comparator2
*/
function lexicographic(compare1, compare2) {
return function (a, b) {
var result = compare1(a, b);
if (result === 0) {
return compare2(a, b);
} else {
return result;
}
};
}
/*
* A comparator for identifiers: the lexicographic combination of
* scope, position and name.
*
* @param {number} pos - the target position by which identifiers are
* compared
* @return {Function} - the comparator function
*/
function compareIdentifiers(pos) {
return lexicographic(compareByScope,
lexicographic(compareByPosition(pos),
compareByName));
}
/*
* A comparator for properties: the lexicographic combination of
* association, path name, position, and name.
*
* @param {Object} assoc - the association by which properties are
* compared
* @param {string} path - the path name by which properties are
* compared
* @param {number} pos - the target position by which properties are
* compared
* @return {Function} - the comparator function
*/
function compareProperties(assoc, path, pos) {
return lexicographic(compareByAssociation(assoc),
lexicographic(compareByPath(path),
lexicographic(compareByPosition(pos),
compareByName)));
}
/*
* Clone a list of hints. (Used so that later annotations are not
* preserved when scope information changes.)
*
* @param {Array.<Object>} hints - an array of hint tokens
* @return {Array.<Object>} - a new array of objects that are clones of
* the objects in the input array
*/
function copyHints(hints) {
function cloneToken(token) {
var copy = {},
prop;
for (prop in token) {
if (Object.prototype.hasOwnProperty.call(token, prop)) {
copy[prop] = token[prop];
}
}
return copy;
}
return hints.map(cloneToken);
}
var cursor = this.editor.getCursorPos(),
offset = this.editor.indexFromPos(cursor),
var MAX_DISPLAYED_HINTS = 500,
type = this.getType(),
association,
builtins = this.builtins,
hints;
if (type.property) {
hints = copyHints(this.properties);
if (type.context &&
Object.prototype.hasOwnProperty.call(this.associations, type.context)) {
association = this.associations[type.context];
hints = HintUtils.annotateWithAssociation(hints, association);
hints.sort(compareProperties(association, this.path, offset));
} else {
hints.sort(compareProperties({}, this.path, offset));
}
} else {
hints = copyHints(this.identifiers);
hints = HintUtils.annotateWithScope(hints, this.scope);
hints = hints.concat(this.literals);
hints.sort(compareIdentifiers(offset));
hints = hints.concat(this.globals);
hints = hints.concat(HintUtils.LITERALS);
hints = hints.concat(HintUtils.KEYWORDS);
/**
* Is the origin one of the builtin files.
*
* @param {string} origin
*/
function isBuiltin(origin) {
return builtins.indexOf(origin) !== -1;
}
/**
* Filter an array hints using a given query and matcher.
* The hints are returned in the format of the matcher.
* The matcher returns the value in the "label" property,
* the match score in "matchGoodness" property.
*
* @param {Array} hints - array of hints
* @param {StringMatcher} matcher
* @returns {Array} - array of matching hints.
*/
function filterWithQueryAndMatcher(hints, matcher) {
var matchResults = $.map(hints, function (hint) {
var searchResult = matcher.match(hint.value, query);
if (searchResult) {
searchResult.value = hint.value;
searchResult.guess = hint.guess;
if (hint.depth !== undefined) {
searchResult.depth = hint.depth;
}
if (!type.property && !type.showFunctionType && hint.origin &&
isBuiltin(hint.origin)) {
searchResult.builtin = 1;
} else {
searchResult.builtin = 0;
}
}
return searchResult;
});
return matchResults;
}
if (type.property) {
hints = this.ternHints || [];
hints = filterWithQueryAndMatcher(hints, matcher);
// If there are no hints then switch over to guesses.
if (hints.length === 0) {
hints = filterWithQueryAndMatcher(this.ternProperties, matcher);
}
StringMatch.multiFieldSort(hints, { matchGoodness: 0, value: 1 });
} else if (type.showFunctionType) {
hints = this.getFunctionTypeHint();
} else { // identifiers, literals, and keywords
hints = this.ternHints || [];
hints = hints.concat(HintUtils.LITERALS);
hints = hints.concat(HintUtils.KEYWORDS);
hints = filterWithQueryAndMatcher(hints, matcher);
StringMatch.multiFieldSort(hints, { matchGoodness: 0, depth: 1, builtin: 2, value: 3 });
}
if (hints.length > MAX_DISPLAYED_HINTS) {
hints = hints.slice(0, MAX_DISPLAYED_HINTS);
}
return hints;
};
Session.prototype.setTernHints = function (newHints) {
this.ternHints = newHints;
};
Session.prototype.setTernProperties = function (newProperties) {
this.ternProperties = newProperties;
};
Session.prototype.setFnType = function (newFnType) {
this.fnType = newFnType;
};
/**
* Get the function type hint. This will format the hint so
* that it has the called variable name instead of just "fn()".
*/
Session.prototype.getFunctionTypeHint = function () {
var fnHint = this.fnType,
hints = [];
if (fnHint && (fnHint.substring(0, 3) === "fn(")) {
var sessionType = this.getType(),
cursor = sessionType.functionCallPos,
token = cursor ? this.getToken(cursor) : undefined,
varName;
if (token) {
varName = token.string;
if (varName) {
fnHint = varName + fnHint.substr(2);
}
}
hints[0] = {value: fnHint, positions: []};
}
return hints;
};
module.exports = Session;
});

View File

@ -0,0 +1,5 @@
{
"jumptoDefinition": [
"Ctrl-J"
]
}

View File

@ -29,20 +29,28 @@ define(function (require, exports, module) {
var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
EditorManager = brackets.getModule("editor/EditorManager"),
DocumentManager = brackets.getModule("document/DocumentManager"),
Commands = brackets.getModule("command/Commands"),
CommandManager = brackets.getModule("command/CommandManager"),
Menus = brackets.getModule("command/Menus"),
Strings = brackets.getModule("strings"),
AppInit = brackets.getModule("utils/AppInit"),
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
StringUtils = brackets.getModule("utils/StringUtils"),
StringMatch = brackets.getModule("utils/StringMatch"),
HintUtils = require("HintUtils"),
ScopeManager = require("ScopeManager"),
Session = require("Session");
var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));
var JUMPTO_DEFINITION = "navigate.jumptoDefinition";
var session = null, // object that encapsulates the current session state
cachedHints = null, // sorted hints for the current hinting session
cachedType = null, // describes the lookup type and the object context
cachedScope = null, // the inner-most scope returned by the query worker
cachedLine = null; // the line number for the cached scope
var MAX_DISPLAYED_HINTS = 100;
cachedLine = null, // the line number for the cached scope
matcher = null; // string matcher for hints
/**
* Creates a hint response object. Filters the hint list using the query
@ -51,75 +59,14 @@ define(function (require, exports, module) {
*
* @param {Array.<Object>} hints - hints to be included in the response
* @param {string} query - querystring with which to filter the hint list
* @param {Object} type - the type of query, property vs. identifier
* @return {Object} - hint response as defined by the CodeHintManager API
*/
function getHintResponse(hints, query) {
function getHintResponse(hints, query, type) {
var trimmedQuery,
filteredHints,
formattedHints;
/*
* Filter a list of tokens using the query string in the closure.
*
* @param {Array.<Object>} tokens - list of hints to filter
* @param {number} limit - maximum numberof tokens to return
* @return {Array.<Object>} - filtered list of hints
*/
function filterWithQuery(tokens, limit) {
/*
* Filter arr using test, returning at most limit results from the
* front of the array.
*
* @param {Array} arr - array to filter
* @param {Function} test - test to determine if an element should
* be included in the results
* @param {number} limit - the maximum number of elements to return
* @return {Array.<Object>} - new array of filtered elements
*/
function filterArrayPrefix(arr, test, limit) {
var i = 0,
results = [],
elem;
for (i; i < arr.length && results.length <= limit; i++) {
elem = arr[i];
if (test(elem)) {
results.push(elem);
}
}
return results;
}
// If the query is a string literal (i.e., if it starts with a
// string literal delimiter, and hence if trimmedQuery !== query)
// then only string literal hints should be returned, and matching
// should be performed w.r.t. trimmedQuery. If the query is
// otherwise non-empty, no string literals should match. If the
// query is empty then no hints are filtered.
if (trimmedQuery !== query) {
return filterArrayPrefix(tokens, function (token) {
if (token.literal && token.kind === "string") {
return (token.value.indexOf(trimmedQuery) === 0);
} else {
return false;
}
}, limit);
} else if (query.length > 0) {
return filterArrayPrefix(tokens, function (token) {
if (token.literal && token.kind === "string") {
return false;
} else {
return (token.value.indexOf(query) === 0);
}
}, limit);
} else {
return tokens.slice(0, limit);
}
}
/*
* Returns a formatted list of hints with the query substring
* highlighted.
@ -127,61 +74,55 @@ define(function (require, exports, module) {
* @param {Array.<Object>} hints - the list of hints to format
* @param {string} query - querystring used for highlighting matched
* poritions of each hint
* @param {Array.<jQuery.Object>} - array of hints formatted as jQuery
* @return {Array.<jQuery.Object>} - array of hints formatted as jQuery
* objects
*/
function formatHints(hints, query) {
return hints.map(function (token) {
var hint = token.value,
index = hint.indexOf(query),
$hintObj = $("<span>").addClass("brackets-js-hints"),
delimiter = "";
var $hintObj = $("<span>").addClass("brackets-js-hints");
// level indicates either variable scope or property confidence
switch (token.level) {
case 0:
$hintObj.addClass("priority-high");
break;
case 1:
$hintObj.addClass("priority-medium");
break;
case 2:
$hintObj.addClass("priority-low");
break;
}
// is the token a global variable?
if (token.global) {
$hintObj.addClass("global-hint");
}
// is the token a literal?
if (token.literal) {
$hintObj.addClass("literal-hint");
if (token.kind === "string") {
delimiter = HintUtils.DOUBLE_QUOTE;
if (!type.property && token.depth !== undefined) {
switch (token.depth) {
case 0:
$hintObj.addClass("priority-high");
break;
case 1:
$hintObj.addClass("priority-medium");
break;
case 2:
$hintObj.addClass("priority-low");
break;
default:
$hintObj.addClass("priority-lowest");
break;
}
}
if (token.guess) {
$hintObj.addClass("guess-hint");
}
// is the token a keyword?
if (token.keyword) {
$hintObj.addClass("keyword-hint");
}
// higlight the matched portion of each hint
if (index >= 0) {
var prefix = StringUtils.htmlEscape(hint.slice(0, index)),
match = StringUtils.htmlEscape(hint.slice(index, index + query.length)),
suffix = StringUtils.htmlEscape(hint.slice(index + query.length));
$hintObj.append(delimiter + prefix)
.append($("<span>")
.append(match)
.addClass("matched-hint"))
.append(suffix + delimiter);
// highlight the matched portion of each hint
if (token.stringRanges) {
token.stringRanges.forEach(function (item) {
if (item.matched) {
$hintObj.append($("<span>")
.append(StringUtils.htmlEscape(item.text))
.addClass("matched-hint"));
} else {
$hintObj.append(StringUtils.htmlEscape(item.text));
}
});
} else {
$hintObj.text(delimiter + hint + delimiter);
$hintObj.text(token.value);
}
$hintObj.data("token", token);
return $hintObj;
@ -200,8 +141,11 @@ define(function (require, exports, module) {
trimmedQuery = query;
}
filteredHints = filterWithQuery(hints, MAX_DISPLAYED_HINTS);
formattedHints = formatHints(filteredHints, trimmedQuery);
if (hints) {
formattedHints = formatHints(hints, trimmedQuery);
} else {
formattedHints = [];
}
return {
hints: formattedHints,
@ -224,25 +168,29 @@ define(function (require, exports, module) {
* @return {boolean} - can the provider provide hints for this session?
*/
JSHints.prototype.hasHints = function (editor, key) {
if (session && ((key === null) || HintUtils.maybeIdentifier(key))) {
if (session && HintUtils.hintableKey(key)) {
var cursor = session.getCursor(),
token = session.getToken(cursor);
// don't autocomplete within strings or comments, etc.
if (token && HintUtils.hintable(token)) {
var offset = session.getOffset();
var offset = session.getOffset(),
type = session.getType(),
query = session.getQuery();
// Invalidate cached information if: 1) no scope exists; 2) the
// cursor has moved a line; 3) the scope is dirty; or 4) if the
// cursor has moved into a different scope. Cached information
// is also reset on editor change.
if (!cachedScope ||
if (!cachedHints ||
cachedLine !== cursor.line ||
ScopeManager.isScopeDirty(session.editor.document) ||
!cachedScope.containsPositionImmediate(offset)) {
cachedScope = null;
type.property !== cachedType.property ||
type.context !== cachedType.context ||
type.showFunctionType !== cachedType.showFunctionType) {
//console.log("clear hints");
cachedLine = null;
cachedHints = null;
matcher = null;
}
return true;
}
@ -261,61 +209,54 @@ define(function (require, exports, module) {
JSHints.prototype.getHints = function (key) {
var cursor = session.getCursor(),
token = session.getToken(cursor);
if ((key === null) || HintUtils.hintable(token)) {
if (token) {
if (!cachedScope) {
var offset = session.getOffset(),
scopeResponse = ScopeManager.getScopeInfo(session.editor.document, offset),
self = this;
if (token && HintUtils.hintableKey(key) && HintUtils.hintable(token)) {
var type = session.getType(),
query = session.getQuery();
if (scopeResponse.hasOwnProperty("promise")) {
var $deferredHints = $.Deferred();
scopeResponse.promise.done(function (scopeInfo) {
session.setScopeInfo(scopeInfo);
cachedScope = scopeInfo.scope;
cachedLine = cursor.line;
cachedType = session.getType();
cachedHints = session.getHints();
// Compute fresh hints if none exist, or if the session
// type has changed since the last hint computation
if (!cachedHints ||
type.property !== cachedType.property ||
type.context !== cachedType.context ||
type.showFunctionType !== cachedType.showFunctionType || query.length === 0) {
var offset = session.getOffset(),
scopeResponse = ScopeManager.requestHints(session, session.editor.document, offset),
self = this;
$(self).triggerHandler("resolvedResponse", [cachedHints, cachedType]);
if ($deferredHints.state() === "pending") {
var query = session.getQuery(),
hintResponse = getHintResponse(cachedHints, query);
$deferredHints.resolveWith(null, [hintResponse]);
$(self).triggerHandler("hintResponse", [query]);
}
}).fail(function () {
if ($deferredHints.state() === "pending") {
$deferredHints.reject();
}
});
$(this).triggerHandler("deferredResponse");
return $deferredHints;
} else {
session.setScopeInfo(scopeResponse);
cachedScope = scopeResponse.scope;
if (scopeResponse.hasOwnProperty("promise")) {
var $deferredHints = $.Deferred();
scopeResponse.promise.done(function () {
cachedLine = cursor.line;
}
}
cachedType = session.getType();
matcher = new StringMatch.StringMatcher();
cachedHints = session.getHints(query, matcher);
if (cachedScope) {
var type = session.getType(),
query = session.getQuery();
$(self).triggerHandler("resolvedResponse", [cachedHints, cachedType]);
// Compute fresh hints if none exist, or if the session
// type has changed since the last hint computation
if (!cachedHints ||
type.property !== cachedType.property ||
type.context !== cachedType.context) {
cachedType = type;
cachedHints = session.getHints();
}
return getHintResponse(cachedHints, query);
if ($deferredHints.state() === "pending") {
query = session.getQuery();
var hintResponse = getHintResponse(cachedHints, query, type);
$deferredHints.resolveWith(null, [hintResponse]);
$(self).triggerHandler("hintResponse", [query]);
}
}).fail(function () {
if ($deferredHints.state() === "pending") {
$deferredHints.reject();
}
});
$(this).triggerHandler("deferredResponse");
return $deferredHints;
} else {
cachedLine = cursor.line;
}
}
if (cachedHints) {
cachedHints = session.getHints(session.getQuery(), matcher);
return getHintResponse(cachedHints, query, type);
}
}
return null;
@ -336,7 +277,7 @@ define(function (require, exports, module) {
query = session.getQuery(),
start = {line: cursor.line, ch: cursor.ch - query.length},
end = {line: cursor.line, ch: (token ? token.end : cursor.ch)},
delimeter;
delimiter;
if (token && token.string === ".") {
var nextCursor = session.getNextCursorOnLine(),
@ -353,18 +294,23 @@ define(function (require, exports, module) {
// to wrap it, preserving the existing delimiter if possible.
if (hint.literal && hint.kind === "string") {
if (token.string.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
delimeter = HintUtils.DOUBLE_QUOTE;
delimiter = HintUtils.DOUBLE_QUOTE;
} else if (token.string.indexOf(HintUtils.SINGLE_QUOTE) === 0) {
delimeter = HintUtils.SINGLE_QUOTE;
delimiter = HintUtils.SINGLE_QUOTE;
} else {
delimeter = hint.delimeter;
delimiter = hint.delimiter;
}
completion = completion.replace("\\", "\\\\");
completion = completion.replace(delimeter, "\\" + delimeter);
completion = delimeter + completion + delimeter;
completion = completion.replace(delimiter, "\\" + delimiter);
completion = delimiter + completion + delimiter;
}
if (session.getType().showFunctionType) {
// function types show up as hints, so don't insert anything
// if we were displaying a function type
return false;
}
// Replace the current token with the completion
session.editor.document.replaceRange(completion, start, end);
@ -385,6 +331,7 @@ define(function (require, exports, module) {
function initializeSession(editor) {
ScopeManager.handleEditorChange(editor.document);
session = new Session(editor);
cachedHints = null;
}
/*
@ -395,7 +342,6 @@ define(function (require, exports, module) {
*/
function installEditorListeners(editor) {
// always clean up cached scope and hint info
cachedScope = null;
cachedLine = null;
cachedHints = null;
cachedType = null;
@ -436,6 +382,49 @@ define(function (require, exports, module) {
installEditorListeners(current);
}
/*
* Handle JumptoDefiniton menu/keyboard command.
*/
function handleJumpToDefinition() {
var offset = session.getOffset(),
response = ScopeManager.requestJumptoDef(session, session.editor.document, offset);
if (response.hasOwnProperty("promise")) {
response.promise.done(function (jumpResp) {
if (jumpResp.resultFile) {
if (jumpResp.resultFile !== jumpResp.file) {
var resolvedPath = ScopeManager.getResolvedPath(jumpResp.resultFile);
if (resolvedPath) {
CommandManager.execute(Commands.FILE_OPEN, {fullPath: resolvedPath})
.done(function () {
session.editor.setCursorPos(jumpResp.start);
session.editor.setSelection(jumpResp.start, jumpResp.end);
session.editor.centerOnCursor();
});
}
} else {
session.editor.setCursorPos(jumpResp.start);
session.editor.setSelection(jumpResp.start, jumpResp.end);
session.editor.centerOnCursor();
}
}
}).fail(function () {
response.reject();
});
}
}
// Register command handler
CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, JUMPTO_DEFINITION, handleJumpToDefinition);
// Add the menu item
var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU);
if (menu) {
menu.addMenuItem(JUMPTO_DEFINITION, KeyboardPrefs.jumptoDefinition, Menus.BEFORE, Commands.NAVIGATE_GOTO_DEFINITION);
}
ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");
// uninstall/install change listener as the active editor changes
@ -452,5 +441,6 @@ define(function (require, exports, module) {
// for unit testing
exports.jsHintProvider = jsHints;
exports.initializeSession = initializeSession;
exports.handleJumpToDefinition = handleJumpToDefinition;
});
});

View File

@ -1,311 +0,0 @@
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global self, importScripts, setTimeout */
/**
* Loads a file that contains an AMD module definition using the web worker
* importScripts global function.
*
* @param {string} url - the URL of the module to load
* @return {Object} - the module imported from the aforementioned URL
*/
function require(url) {
"use strict";
var exports = {},
oldDefine = self.define;
/*
* The following function is called by AMD modules when loaded with
* importScripts. CommonJS-style modules will only pass a wrapper function
* as a single argument; proper AMD-style modules may additionally pass an
* array of bindings, which are ignored. In the former case, the wrapper
* function expects require, exports and module arguments; in the latter
* case, the wrapper function expects only an exports arguments.
*
* @param {?Array.<string>} bindings - an optional list of resources
* required by the module
* @param {Function} wrapper - the function that defines the module
*/
self.define = function (bindings, wrapper) {
var module = { exports: exports },
require = null;
if (typeof bindings === "function") {
wrapper = bindings;
} else {
require = module.exports;
}
wrapper(require, module.exports, module);
exports = module.exports;
};
self.define.amd = true;
importScripts(url);
self.define = oldDefine;
return exports;
}
(function () {
"use strict";
var Scope = require("Scope.js"),
HintUtils = require("HintUtils.js"),
esprima = require("thirdparty/esprima/esprima.js");
// maximum number of times a file will be reparsed before failing
var MAX_RETRIES = 10;
/**
* Send a log message back from the worker to the main thread
*
* @param {string} msg - the log message
*/
function _log(msg) {
self.postMessage({log: msg });
}
/**
* Walk the scope to find all the objects of a given type, along with a
* list of their positions in the file.
*
* @param {Function} walk - the function used to walk over an implicit scope
* @param {string} keyProp - the property of the implicit scope to walk over
* @return {Array.<Object>} - a list of hint tokens for the implicit scope,
* including the positions at which they occur.
*/
function siftPositions(walk, keyProp) {
var occurrences,
results = [],
key,
token,
comparator = function (a, b) {
return a - b;
};
occurrences = walk(function (acc, token) {
if (Object.prototype.hasOwnProperty.call(acc, token[keyProp])) {
acc[token[keyProp]].push(token.range[0]);
} else {
acc[token[keyProp]] = [token.range[0]];
}
return acc;
}, {});
for (key in occurrences) {
if (Object.prototype.hasOwnProperty.call(occurrences, key)) {
token = HintUtils.makeToken(key, occurrences[key].sort(comparator));
results.push(token);
}
}
return results;
}
/**
* Walk the scope to compute all available association objects
*
* @param {Function} walk - the function used to walk over an implicit scope
* @return {Object} - a set of association objects: each property is a
* property name; each value is an association, which indicates the
* number of times the property occurs w.r.t. to a particular lookup
* context.
*/
function siftAssociations(walk) {
return walk(function (acc, assoc) {
var obj = assoc.object,
prop = assoc.property;
if (Object.prototype.hasOwnProperty.call(acc, obj.name)) {
if (Object.prototype.hasOwnProperty.call(acc[obj.name], prop.name)) {
acc[obj.name][prop.name]++;
} else {
acc[obj.name][prop.name] = 1;
}
} else {
acc[obj.name] = {};
acc[obj.name][prop.name] = 1;
}
return acc;
}, {});
}
/**
* Parse JSLint globals annotations from an array of JavaScript comments
*
* @param {Array.<string>} comments - list of JavaScript comments
* @return {Array.<Object>} - a list of global hint objects as described by
* JSLint annotations found in the comments.
*/
function extractGlobals(comments) {
var globals = [];
if (comments) {
comments.forEach(function (comment) {
if (comment.type === "Block" && comment.value) {
var val = comment.value;
if (val.indexOf("global") === 0) {
val.substring(7).split(",").forEach(function (global) {
var index = global.indexOf(":");
if (index >= 0) {
global = global.substring(0, index);
}
globals.push(HintUtils.makeToken(global.trim()));
});
} else if (val.indexOf("jslint") === 0) {
val.substring(7).split(",").forEach(function (ann) {
var index = ann.indexOf(":"),
aKey = (index >= 0) ? ann.substring(0, index).trim() : "",
aVal = (index >= 0) ? ann.substring(index + 1, ann.length).trim() : "";
if (aVal === "true" && HintUtils.JSL_GLOBAL_DEFS.hasOwnProperty(aKey)) {
globals = globals.concat(HintUtils.JSL_GLOBAL_DEFS[aKey]);
}
});
}
}
});
}
return globals;
}
/**
* Send the scope and associated parse information back to the caller.
* Called by the parse function below.
*
* @param {string} dir - the directory name of the parsed file
* @param {string} file - the file name of the parsed file
* @param {number} length - the length of the parsed file
* @param {Object} parseObj - scope and token data from the parsed file
*/
function respond(dir, file, length, parseObj) {
var success = !!parseObj,
response = {
type : HintUtils.SCOPE_MSG_TYPE,
dir : dir,
file : file,
length : length,
success : success
};
if (success) {
var scope = parseObj.scope,
globals = parseObj.globals,
identifiers = siftPositions(scope.walkDownIdentifiers.bind(scope), "name"),
properties = siftPositions(scope.walkDownProperties.bind(scope), "name"),
literals = siftPositions(scope.walkDownLiterals.bind(scope), "value"),
associations = siftAssociations(scope.walkDownAssociations.bind(scope));
response.scope = scope;
response.globals = HintUtils.annotateGlobals(globals);
response.identifiers = identifiers;
response.properties = HintUtils.annotateWithPath(properties, dir, file);
response.literals = HintUtils.annotateLiterals(literals, "string");
response.associations = associations;
}
self.postMessage(response);
}
/**
* Parse a JavaScript text with Esprima. This function is intended to be
* called asynchronously; the respond function above is called with the
* results of parsing.
*
* @param {string} dir - the directory name of the file to parse
* @param {string} file - the file name of the file to parse
* @param {string} text - the text of the file to parse
* @param {number} retries - the number of times an unparseable text should
* be retried, after blanking whatever lines are causing the parsing
* errors.
*/
function parse(dir, file, text, retries) {
try {
var ast = esprima.parse(text, {
range : true,
tolerant : true,
comment : true
});
var scope = new Scope(ast),
definedGlobals = extractGlobals(ast.comments),
builtinGlobals = HintUtils.BUILTIN_GLOBALS,
allGlobals = definedGlobals.concat(builtinGlobals),
comparator = function (a, b) { return a.value < b.value; };
respond(dir, file, text.length, {
scope : scope,
globals : allGlobals.sort(comparator)
});
} catch (err) {
// If parsing fails, we can try again after blanking out the line
// that caused the parse error. This is unreliable though, because
// the line number on which the parser fails is not necessarily the
// best line to remove. Some errors will cause the entire remainder
// of the file to be blanked out, never resulting in a parseable
// file. Consequently, this is attempted only when necessary; i.e.,
// when the request.force flag is set.
// Inspired by fuckit.js: https://github.com/mattdiamond/fuckitjs
// _log("Parsing failed: " + err + " at " + err.index);
if (retries > 0) {
var lines = text.split("\n"),
lineno = Math.min(lines.length, err.lineNumber) - 1,
newline,
removed;
// Blank the offending line and start over
if (-1 < lineno < lines.length) {
newline = lines[lineno].replace(/./g, " ");
if (newline !== lines[lineno]) {
removed = lines.splice(lineno, 1, newline);
if (removed && removed.length > 0) {
setTimeout(function () {
parse(dir, file, lines.join("\n"), --retries);
}, 0);
return;
}
}
}
}
respond(dir, file, text.length, null);
}
}
self.addEventListener("message", function (e) {
var request = e.data,
type = request.type;
if (type === HintUtils.SCOPE_MSG_TYPE) {
var dir = request.dir,
file = request.file,
text = request.text,
retries = request.force ? MAX_RETRIES : 0;
setTimeout(function () { parse(dir, file, text, retries); }, 0);
} else {
_log("Unknown message: " + JSON.stringify(request));
}
});
}());

View File

@ -33,16 +33,16 @@
color: rgb(0, 0, 100); /* blue */
}
.brackets-js-hints.priority-lowest {
color: rgb(0, 0, 0); /* black */
}
.brackets-js-hints.variable-hint {
}
.brackets-js-hints.property-hint {
}
.brackets-js-hints.global-hint {
font-style: italic;
}
.brackets-js-hints.literal-hint {
color: rgb(50, 50, 50); /* dark grey */
}
@ -54,3 +54,9 @@
.brackets-js-hints .matched-hint {
font-weight: bold;
}
.brackets-js-hints.guess-hint {
font-style: italic;
}

View File

@ -0,0 +1,296 @@
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global self, importScripts, require */
importScripts("thirdparty/requirejs/require.js");
(function () {
"use strict";
var HintUtils;
var Tern;
require(["./HintUtils"], function (hintUtils) {
HintUtils = hintUtils;
var ternRequire = require.config({baseUrl: "./thirdparty"});
ternRequire(["tern/lib/tern", "tern/plugin/requirejs"], function (tern, requirejs) {
Tern = tern;
});
});
var ternServer = null;
// Save the tern callbacks for when we get the contents of the file
var fileCallBacks = {};
/**
* Provide the contents of the requested file to tern
*/
function getFile(name, next) {
// save the callback
fileCallBacks[name] = next;
// post a message back to the main thread to get the file contents
self.postMessage({
type: HintUtils.TERN_GET_FILE_MSG,
file: name
});
}
/**
* Handle a response from the main thread providing the contents of a file
* @param {string} file - the name of the file
* @param {string} text - the contents of the file
*/
function handleGetFile(file, text) {
var next = fileCallBacks[file];
if (next) {
next(null, text);
}
delete fileCallBacks[file];
}
/**
* Create a new tern server.
*/
function initTernServer(env, dir, files) {
var ternOptions = {
defs: env,
async: true,
getFile: getFile,
plugins: {requirejs: {}}
};
ternServer = new Tern.Server(ternOptions);
files.forEach(function (file) {
ternServer.addFile(file);
});
}
function buildRequest(dir, file, query, offset, text) {
query = {type: query};
query.start = offset;
query.end = offset;
query.file = file;
query.filter = false;
query.sort = false;
query.depths = true;
query.guess = true;
query.origins = true;
query.expandWordForward = true;
var request = {query: query, files: [], offset: offset};
request.files.push({type: "full", name: file, text: text});
return request;
}
/**
* Send a log message back from the worker to the main thread
*
* @param {string} msg - the log message
*/
function _log(msg) {
self.postMessage({log: msg });
}
/**
* Get definition location
* @param {string} dir - the directory
* @param {string} file - the file name
* @param {number} offset - the offset into the file for cursor
* @param {string} text - the text of the file
*/
function getJumptoDef(dir, file, offset, text) {
var request = buildRequest(dir, file, "definition", offset, text);
request.query.lineCharPositions = true;
ternServer.request(request, function (error, data) {
if (error) {
_log("Error returned from Tern 'definition' request: " + error);
self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG});
return;
}
// Post a message back to the main thread with the definition
self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG,
file: file,
resultFile: data.file,
offset: offset,
start: data.start,
end: data.end
});
});
}
/**
* Get the completions for the given offset
* @param {string} dir - the directory
* @param {string} file - the file name
* @param {number} offset - the offset into the file where we want completions for
* @param {string} text - the text of the file
*/
function getTernHints(dir, file, offset, text) {
var request = buildRequest(dir, file, "completions", offset, text),
i;
//_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
ternServer.request(request, function (error, data) {
var completions = [];
if (error) {
_log("Error returned from Tern 'completions' request: " + error);
} else {
//_log("found " + data.completions.length + " for " + file + "@" + offset);
for (i = 0; i < data.completions.length; ++i) {
var completion = data.completions[i];
completions.push({value: completion.name, type: completion.type, depth: completion.depth,
guess: completion.guess, origin: completion.origin});
}
}
// Post a message back to the main thread with the completions
self.postMessage({type: HintUtils.TERN_COMPLETIONS_MSG,
dir: dir,
file: file,
offset: offset,
completions: completions
});
});
}
/**
* Get all the known properties for guessing.
*
* @param {string} dir - the directory
* @param {string} file - the file name
* @param {string} text - the text of the file
*/
function handleGetProperties(dir, file, offset, text) {
var request = buildRequest(dir, file, "properties", undefined, text),
i;
//_log("request " + request.type + dir + " " + file);
ternServer.request(request, function (error, data) {
var properties = [];
if (error) {
_log("Error returned from Tern 'properties' request: " + error);
} else {
//_log("completions = " + data.completions.length);
for (i = 0; i < data.completions.length; ++i) {
var property = data.completions[i];
properties.push({value: property, guess: true});
}
}
// Post a message back to the main thread with the completions
self.postMessage({type: HintUtils.TERN_GET_PROPERTIES_MSG,
dir: dir,
file: file,
offset: offset,
properties: properties
});
});
}
/**
* Get the function type for the given offset
* @param {string} dir - the directory
* @param {string} file - the file name
* @param {number} offset - the offset into the file where we want completions for
* @param {string} text - the text of the file
*/
function handleFunctionType(dir, file, pos, offset, text) {
var request = buildRequest(dir, file, "type", pos, text);
request.preferFunction = true;
//_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
ternServer.request(request, function (error, data) {
var fnType = "";
if (error) {
_log("Error returned from Tern 'type' request: " + error);
} else {
fnType = data.type;
}
// Post a message back to the main thread with the completions
self.postMessage({type: HintUtils.TERN_CALLED_FUNC_TYPE_MSG,
dir: dir,
file: file,
offset: offset,
fnType: fnType
});
});
}
self.addEventListener("message", function (e) {
var dir, file, text, offset,
request = e.data,
type = request.type;
if (type === HintUtils.TERN_INIT_MSG) {
dir = request.dir;
var env = request.env,
files = request.files;
initTernServer(env, dir, files);
} else if (type === HintUtils.TERN_COMPLETIONS_MSG) {
dir = request.dir;
file = request.file;
text = request.text;
offset = request.offset;
getTernHints(dir, file, offset, text);
} else if (type === HintUtils.TERN_GET_PROPERTIES_MSG) {
file = request.file;
dir = request.dir;
text = request.text;
offset = request.offset;
handleGetProperties(dir, file, offset, text);
} else if (type === HintUtils.TERN_GET_FILE_MSG) {
file = request.file;
text = request.text;
handleGetFile(file, text);
} else if (type === HintUtils.TERN_CALLED_FUNC_TYPE_MSG) {
dir = request.dir;
file = request.file;
text = request.text;
offset = request.offset;
var pos = request.pos;
handleFunctionType(dir, file, pos, offset, text);
} else if (type === HintUtils.TERN_JUMPTODEF_MSG) {
file = request.file;
dir = request.dir;
text = request.text;
offset = request.offset;
getJumptoDef(dir, file, offset, text);
} else {
_log("Unknown message: " + JSON.stringify(request));
}
});
}());

View File

@ -0,0 +1,10 @@
/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */
define(function (require, exports, module) {
"use strict";
exports.a = function (a, b) {};
exports.b = function () {
return "a string";
};
exports.j = "hi";
});

View File

@ -1,5 +1,5 @@
/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global brackets, $ */
/*global brackets, require */
var A1 = { propA : 1 },
A2 = { propA : 2 },
@ -20,4 +20,26 @@ function funB(paramB1, paramB2) {
B1.foo = 0;
console.log("hello\\\" world!");
}
var s = "a string";
var r = s;
var t = r;
}
/**
* @param {string} a
* @param {number} b
*/
function funD(a, b) {
"use strict";
return {x: a, y: b};
}
require(["MyModule"], function (myModule) {
'use strict';
var x = myModule.a;
});
funB();
var x = A1;

@ -0,0 +1 @@
Subproject commit f3c70d76efd79040721f913b5877a4f3149fdbe2

View File

@ -1,19 +0,0 @@
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,58 @@
RequireJS is released under two licenses: new BSD, and MIT. You may pick the
license that best suits your development needs. The text of both licenses are
provided below.
The "New" BSD License:
----------------------
Copyright (c) 2010-2011, The Dojo Foundation
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Dojo Foundation nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
MIT License
-----------
Copyright (c) 2010-2011, The Dojo Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,34 @@
/*
RequireJS 2.1.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
Available via the MIT or new BSD license.
see: http://github.com/jrburke/requirejs for details
*/
var requirejs,require,define;
(function(W){function D(b){return M.call(b)==="[object Function]"}function E(b){return M.call(b)==="[object Array]"}function t(b,c){if(b){var d;for(d=0;d<b.length;d+=1)if(b[d]&&c(b[d],d,b))break}}function N(b,c){if(b){var d;for(d=b.length-1;d>-1;d-=1)if(b[d]&&c(b[d],d,b))break}}function A(b,c){for(var d in b)if(b.hasOwnProperty(d)&&c(b[d],d))break}function O(b,c,d,g){c&&A(c,function(c,j){if(d||!F.call(b,j))g&&typeof c!=="string"?(b[j]||(b[j]={}),O(b[j],c,d,g)):b[j]=c});return b}function r(b,c){return function(){return c.apply(b,
arguments)}}function X(b){if(!b)return b;var c=W;t(b.split("."),function(b){c=c[b]});return c}function G(b,c,d,g){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=g;if(d)c.originalError=d;return c}function ba(){if(H&&H.readyState==="interactive")return H;N(document.getElementsByTagName("script"),function(b){if(b.readyState==="interactive")return H=b});return H}var g,s,u,y,q,B,H,I,Y,Z,ca=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,da=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
$=/\.js$/,ea=/^\.\//;s=Object.prototype;var M=s.toString,F=s.hasOwnProperty,fa=Array.prototype.splice,v=!!(typeof window!=="undefined"&&navigator&&document),aa=!v&&typeof importScripts!=="undefined",ga=v&&navigator.platform==="PLAYSTATION 3"?/^complete$/:/^(complete|loaded)$/,R=typeof opera!=="undefined"&&opera.toString()==="[object Opera]",w={},n={},P=[],J=!1;if(typeof define==="undefined"){if(typeof requirejs!=="undefined"){if(D(requirejs))return;n=requirejs;requirejs=void 0}typeof require!=="undefined"&&
!D(require)&&(n=require,require=void 0);g=requirejs=function(b,c,d,p){var i,j="_";!E(b)&&typeof b!=="string"&&(i=b,E(c)?(b=c,c=d,d=p):b=[]);if(i&&i.context)j=i.context;(p=w[j])||(p=w[j]=g.s.newContext(j));i&&p.configure(i);return p.require(b,c,d)};g.config=function(b){return g(b)};g.nextTick=typeof setTimeout!=="undefined"?function(b){setTimeout(b,4)}:function(b){b()};require||(require=g);g.version="2.1.1";g.jsExtRegExp=/^\/|:|\?|\.js$/;g.isBrowser=v;s=g.s={contexts:w,newContext:function(b){function c(a,
f,x){var e,m,b,c,d,h,i,g=f&&f.split("/");e=g;var j=k.map,l=j&&j["*"];if(a&&a.charAt(0)===".")if(f){e=k.pkgs[f]?g=[f]:g.slice(0,g.length-1);f=a=e.concat(a.split("/"));for(e=0;f[e];e+=1)if(m=f[e],m===".")f.splice(e,1),e-=1;else if(m==="..")if(e===1&&(f[2]===".."||f[0]===".."))break;else e>0&&(f.splice(e-1,2),e-=2);e=k.pkgs[f=a[0]];a=a.join("/");e&&a===f+"/"+e.main&&(a=f)}else a.indexOf("./")===0&&(a=a.substring(2));if(x&&(g||l)&&j){f=a.split("/");for(e=f.length;e>0;e-=1){b=f.slice(0,e).join("/");if(g)for(m=
g.length;m>0;m-=1)if(x=j[g.slice(0,m).join("/")])if(x=x[b]){c=x;d=e;break}if(c)break;!h&&l&&l[b]&&(h=l[b],i=e)}!c&&h&&(c=h,d=i);c&&(f.splice(0,d,c),a=f.join("/"))}return a}function d(a){v&&t(document.getElementsByTagName("script"),function(f){if(f.getAttribute("data-requiremodule")===a&&f.getAttribute("data-requirecontext")===h.contextName)return f.parentNode.removeChild(f),!0})}function p(a){var f=k.paths[a];if(f&&E(f)&&f.length>1)return d(a),f.shift(),h.require.undef(a),h.require([a]),!0}function i(a){var f,
b=a?a.indexOf("!"):-1;b>-1&&(f=a.substring(0,b),a=a.substring(b+1,a.length));return[f,a]}function j(a,f,b,e){var m,K,d=null,g=f?f.name:null,j=a,l=!0,k="";a||(l=!1,a="_@r"+(M+=1));a=i(a);d=a[0];a=a[1];d&&(d=c(d,g,e),K=o[d]);a&&(d?k=K&&K.normalize?K.normalize(a,function(a){return c(a,g,e)}):c(a,g,e):(k=c(a,g,e),a=i(k),d=a[0],k=a[1],b=!0,m=h.nameToUrl(k)));b=d&&!K&&!b?"_unnormalized"+(N+=1):"";return{prefix:d,name:k,parentMap:f,unnormalized:!!b,url:m,originalName:j,isDefine:l,id:(d?d+"!"+k:k)+b}}function n(a){var f=
a.id,b=l[f];b||(b=l[f]=new h.Module(a));return b}function q(a,f,b){var e=a.id,m=l[e];if(F.call(o,e)&&(!m||m.defineEmitComplete))f==="defined"&&b(o[e]);else n(a).on(f,b)}function z(a,f){var b=a.requireModules,e=!1;if(f)f(a);else if(t(b,function(f){if(f=l[f])f.error=a,f.events.error&&(e=!0,f.emit("error",a))}),!e)g.onError(a)}function s(){P.length&&(fa.apply(C,[C.length-1,0].concat(P)),P=[])}function u(a,f,b){var e=a.map.id;a.error?a.emit("error",a.error):(f[e]=!0,t(a.depMaps,function(e,c){var d=e.id,
g=l[d];g&&!a.depMatched[c]&&!b[d]&&(f[d]?(a.defineDep(c,o[d]),a.check()):u(g,f,b))}),b[e]=!0)}function w(){var a,f,b,e,m=(b=k.waitSeconds*1E3)&&h.startTime+b<(new Date).getTime(),c=[],g=[],i=!1,j=!0;if(!S){S=!0;A(l,function(b){a=b.map;f=a.id;if(b.enabled&&(a.isDefine||g.push(b),!b.error))if(!b.inited&&m)p(f)?i=e=!0:(c.push(f),d(f));else if(!b.inited&&b.fetched&&a.isDefine&&(i=!0,!a.prefix))return j=!1});if(m&&c.length)return b=G("timeout","Load timeout for modules: "+c,null,c),b.contextName=h.contextName,
z(b);j&&t(g,function(a){u(a,{},{})});if((!m||e)&&i)if((v||aa)&&!T)T=setTimeout(function(){T=0;w()},50);S=!1}}function y(a){n(j(a[0],null,!0)).init(a[1],a[2])}function B(a){var a=a.currentTarget||a.srcElement,b=h.onScriptLoad;a.detachEvent&&!R?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=h.onScriptError;a.detachEvent&&!R||a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function I(){var a;for(s();C.length;)if(a=C.shift(),a[0]===
null)return z(G("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));else y(a)}var S,U,h,L,T,k={waitSeconds:7,baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},l={},V={},C=[],o={},Q={},M=1,N=1;L={require:function(a){return a.require?a.require:a.require=h.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=o[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return k.config&&
k.config[a.map.id]||{}},exports:o[a.map.id]}}};U=function(a){this.events=V[a.id]||{};this.map=a;this.shim=k.shim[a.id];this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};U.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=r(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},
defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;h.startTime=(new Date).getTime();var a=this.map;if(this.shim)h.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],r(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;Q[a]||(Q[a]=!0,h.load(this.map.id,a))},check:function(){if(this.enabled&&
!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,m=this.factory;if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(this.depCount<1&&!this.defined){if(D(m)){if(this.events.error)try{e=h.execCb(c,m,b,e)}catch(d){a=d}else e=h.execCb(c,m,b,e);if(this.map.isDefine)if((b=this.module)&&b.exports!==void 0&&b.exports!==this.exports)e=b.exports;else if(e===void 0&&this.usingExports)e=this.exports;if(a)return a.requireMap=this.map,
a.requireModules=[this.map.id],a.requireType="define",z(this.error=a)}else e=m;this.exports=e;if(this.map.isDefine&&!this.ignore&&(o[c]=e,g.onResourceLoad))g.onResourceLoad(h,this.map,this.depMaps);delete l[c];this.defined=!0}this.defining=!1;if(this.defined&&!this.defineEmitted)this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=j(a.prefix);this.depMaps.push(d);q(d,"defined",r(this,function(e){var m,
d;d=this.map.name;var x=this.map.parentMap?this.map.parentMap.name:null,i=h.makeRequire(a.parentMap,{enableBuildCallback:!0,skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,x,!0)})||""),e=j(a.prefix+"!"+d,this.map.parentMap),q(e,"defined",r(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=l[e.id]){this.depMaps.push(e);if(this.events.error)d.on("error",r(this,function(a){this.emit("error",a)}));d.enable()}}else m=r(this,
function(a){this.init([],function(){return a},null,{enabled:!0})}),m.error=r(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];A(l,function(a){a.map.id.indexOf(b+"_unnormalized")===0&&delete l[a.map.id]});z(a)}),m.fromText=r(this,function(b,e){var f=a.name,c=j(f),d=J;e&&(b=e);d&&(J=!1);n(c);try{g.exec(b)}catch(x){throw Error("fromText eval for "+f+" failed: "+x);}d&&(J=!0);this.depMaps.push(c);h.completeLoad(f);i([f],m)}),e.load(a.name,i,m,k)}));h.enable(d,this);this.pluginMaps[d.id]=
d},enable:function(){this.enabling=this.enabled=!0;t(this.depMaps,r(this,function(a,b){var c,e;if(typeof a==="string"){a=j(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=L[a.id]){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",r(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&q(a,"error",this.errback)}c=a.id;e=l[c];!L[c]&&e&&!e.enabled&&h.enable(a,this)}));A(this.pluginMaps,r(this,function(a){var b=l[a.id];b&&!b.enabled&&
h.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){t(this.events[a],function(a){a(b)});a==="error"&&delete this.events[a]}};h={config:k,contextName:b,registry:l,defined:o,urlFetched:Q,defQueue:C,Module:U,makeModuleMap:j,nextTick:g.nextTick,configure:function(a){a.baseUrl&&a.baseUrl.charAt(a.baseUrl.length-1)!=="/"&&(a.baseUrl+="/");var b=k.pkgs,c=k.shim,e={paths:!0,config:!0,map:!0};A(a,function(a,b){e[b]?
b==="map"?O(k[b],a,!0,!0):O(k[b],a,!0):k[b]=a});if(a.shim)A(a.shim,function(a,b){E(a)&&(a={deps:a});if(a.exports&&!a.exportsFn)a.exportsFn=h.makeShimExports(a);c[b]=a}),k.shim=c;if(a.packages)t(a.packages,function(a){a=typeof a==="string"?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ea,"").replace($,"")}}),k.pkgs=b;A(l,function(a,b){if(!a.inited&&!a.map.unnormalized)a.map=j(b)});if(a.deps||a.callback)h.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;
a.init&&(b=a.init.apply(W,arguments));return b||X(a.exports)}},makeRequire:function(a,f){function d(e,c,i){var k,p;if(f.enableBuildCallback&&c&&D(c))c.__requireJsBuild=!0;if(typeof e==="string"){if(D(c))return z(G("requireargs","Invalid require call"),i);if(a&&L[e])return L[e](l[a.id]);if(g.get)return g.get(h,e,a);k=j(e,a,!1,!0);k=k.id;return!F.call(o,k)?z(G("notloaded",'Module name "'+k+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):o[k]}I();h.nextTick(function(){I();p=
n(j(null,a));p.skipMap=f.skipMap;p.init(e,c,i,{enabled:!0});w()});return d}f=f||{};O(d,{isBrowser:v,toUrl:function(b){var d=b.lastIndexOf("."),f=null;d!==-1&&(f=b.substring(d,b.length),b=b.substring(0,d));return h.nameToUrl(c(b,a&&a.id,!0),f)},defined:function(b){b=j(b,a,!1,!0).id;return F.call(o,b)},specified:function(b){b=j(b,a,!1,!0).id;return F.call(o,b)||F.call(l,b)}});if(!a)d.undef=function(b){s();var c=j(b,a,!0),d=l[b];delete o[b];delete Q[c.url];delete V[b];if(d){if(d.events.defined)V[b]=
d.events;delete l[b]}};return d},enable:function(a){l[a.id]&&n(a).enable()},completeLoad:function(a){var b,c,d=k.shim[a]||{},g=d.exports;for(s();C.length;){c=C.shift();if(c[0]===null){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);y(c)}c=l[a];if(!b&&!o[a]&&c&&!c.inited)if(k.enforceDefine&&(!g||!X(g)))if(p(a))return;else return z(G("nodefine","No define call for "+a,null,[a]));else y([a,d.deps||[],d.exportsFn]);w()},nameToUrl:function(a,b){var c,d,i,h,j,l;if(g.jsExtRegExp.test(a))h=a+(b||"");else{c=
k.paths;d=k.pkgs;h=a.split("/");for(j=h.length;j>0;j-=1)if(l=h.slice(0,j).join("/"),i=d[l],l=c[l]){E(l)&&(l=l[0]);h.splice(0,j,l);break}else if(i){c=a===i.name?i.location+"/"+i.main:i.location;h.splice(0,j,c);break}h=h.join("/");h+=b||(/\?/.test(h)?"":".js");h=(h.charAt(0)==="/"||h.match(/^[\w\+\.\-]+:/)?"":k.baseUrl)+h}return k.urlArgs?h+((h.indexOf("?")===-1?"?":"&")+k.urlArgs):h},load:function(a,b){g.load(h,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if(a.type===
"load"||ga.test((a.currentTarget||a.srcElement).readyState))H=null,a=B(a),h.completeLoad(a.id)},onScriptError:function(a){var b=B(a);if(!p(b.id))return z(G("scripterror","Script error",a,[b.id]))}};h.require=h.makeRequire();return h}};g({});t(["toUrl","undef","defined","specified"],function(b){g[b]=function(){var c=w._;return c.require[b].apply(c,arguments)}});if(v&&(u=s.head=document.getElementsByTagName("head")[0],y=document.getElementsByTagName("base")[0]))u=s.head=y.parentNode;g.onError=function(b){throw b;
};g.load=function(b,c,d){var g=b&&b.config||{},i;if(v)return i=g.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),i.type=g.scriptType||"text/javascript",i.charset="utf-8",i.async=!0,i.setAttribute("data-requirecontext",b.contextName),i.setAttribute("data-requiremodule",c),i.attachEvent&&!(i.attachEvent.toString&&i.attachEvent.toString().indexOf("[native code")<0)&&!R?(J=!0,i.attachEvent("onreadystatechange",b.onScriptLoad)):(i.addEventListener("load",
b.onScriptLoad,!1),i.addEventListener("error",b.onScriptError,!1)),i.src=d,I=i,y?u.insertBefore(i,y):u.appendChild(i),I=null,i;else aa&&(importScripts(d),b.completeLoad(c))};v&&N(document.getElementsByTagName("script"),function(b){if(!u)u=b.parentNode;if(q=b.getAttribute("data-main")){if(!n.baseUrl)B=q.split("/"),Y=B.pop(),Z=B.length?B.join("/")+"/":"./",n.baseUrl=Z,q=Y;q=q.replace($,"");n.deps=n.deps?n.deps.concat(q):[q];return!0}});define=function(b,c,d){var g,i;typeof b!=="string"&&(d=c,c=b,b=
null);E(c)||(d=c,c=[]);!c.length&&D(d)&&d.length&&(d.toString().replace(ca,"").replace(da,function(b,d){c.push(d)}),c=(d.length===1?["require"]:["require","exports","module"]).concat(c));if(J&&(g=I||ba()))b||(b=g.getAttribute("data-requiremodule")),i=w[g.getAttribute("data-requirecontext")];(i?i.defQueue:P).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(n)}})(this);

@ -0,0 +1 @@
Subproject commit 14a0211b6af41b05096413e1712eafe367ac0bb1

View File

@ -226,6 +226,23 @@ define(function (require, exports, module) {
});
}
/**
* Find the index of a string in a list of hints.
* @param {Array} hintList - the list of hints
* @param {string} hintSelection - the string represenation of the hint
* to find the index of
* @return {number} the index of the hint corresponding to the hintSelection
*/
function findHint(hintList, hintSelection) {
var i, l;
for (i = 0, l = hintList.length; i < l; ++i) {
var current = hintList[i].data("token");
if (hintSelection === current.value) {
return i;
}
}
return -1;
}
/*
* Simulation of selection of a particular hint in a hint list.
* Presumably results in side effects in the hint provider's
@ -234,18 +251,55 @@ define(function (require, exports, module) {
* @param {Object} provider - a CodeHint provider object
* @param {Object} hintObj - a hint response object from that provider,
* possibly deferred
* @param {number} index - the index into the hint list at which a hint
* is to be selected
* @param {string} hintSelection - the hint to select
*/
function selectHint(provider, hintObj, index) {
function selectHint(provider, hintObj, hintSelection) {
var hintList = expectHints(provider);
_waitForHints(hintObj, function (hintList) {
expect(hintList).not.toBeNull();
var index = findHint(hintList, hintSelection);
expect(hintList[index].data("token")).not.toBeNull();
expect(provider.insertHint(hintList[index])).toBe(false);
});
}
/**
* Wait for the editor to change positions, such as after a jump to
* definition has been triggered. Will timeout after 3 seconds
*
* @param {{line:number, ch:number}} oldLocation - the original line/col
* @param {Function} callback - the callback to apply once the editor has changed position
*/
function _waitForJump(oldLocation, callback) {
waitsFor(function () {
var cursor = testEditor.getCursorPos();
return (cursor.line !== oldLocation.line) ||
(cursor.ch !== oldLocation.ch);
}, "Expected jump did not occur", 3000);
runs(function () { callback(testEditor.getCursorPos()); });
}
/**
* Trigger a jump to definition, and verify that the editor jumped to
* the expected location.
*
* @param {{line:number, ch:number}} expectedLocation - the location the editor should
* jump to.
*/
function editorJumped(expectedLocation) {
var oldLocation = testEditor.getCursorPos();
JSCodeHints.handleJumpToDefinition();
_waitForJump(oldLocation, function (newCursor) {
expect(newCursor.line).toBe(expectedLocation.line);
expect(newCursor.ch).toBe(expectedLocation.ch);
});
}
describe("JavaScript Code Hinting", function () {
beforeEach(function () {
@ -278,13 +332,13 @@ define(function (require, exports, module) {
it("should list declared variable and function names in outer scope", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentExact(hintObj, ["A2", "A3", "funB", "A1"]);
hintsPresent(hintObj, ["A2", "A3", "funB", "A1"]);
});
it("should filter hints by query", function () {
testEditor.setCursorPos({ line: 5, ch: 10 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentExact(hintObj, ["A2", "A3", "A1"]);
hintsPresent(hintObj, ["A1", "A2", "A3"]);
hintsAbsent(hintObj, ["funB"]);
});
@ -293,7 +347,7 @@ define(function (require, exports, module) {
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresent(hintObj, ["break", "case", "catch"]);
});
/*
it("should list explicitly defined globals from JSLint annotations", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
@ -305,7 +359,7 @@ define(function (require, exports, module) {
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresent(hintObj, ["alert", "console", "confirm", "navigator", "window", "frames"]);
});
*/
it("should NOT list implicitly defined globals from missing JSLint annotations", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
@ -339,7 +393,7 @@ define(function (require, exports, module) {
it("should NOT list variables, function names and parameter names in other files", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsAbsent(hintObj, ["D1", "D2", "funE", "E1", "E2"]);
hintsAbsent(hintObj, ["E1", "E2"]);
});
it("should NOT list property names on value lookups", function () {
@ -351,31 +405,31 @@ define(function (require, exports, module) {
it("should list declared variable, function and parameter names in inner scope", function () {
testEditor.setCursorPos({ line: 12, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentExact(hintObj, ["funC", "B2", "B1", "paramB2", "paramB1", "funB", "A2", "A3", "A1"]);
hintsPresent(hintObj, ["B1", "B2", "funC", "paramB1", "paramB2", "funB", "A1", "A2", "A3"]);
});
/*
it("should list string literals that occur in the file", function () {
testEditor.setCursorPos({ line: 12, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresent(hintObj, ["use strict"]);
});
*/
it("should NOT list string literals from other files", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsAbsent(hintObj, ["a very nice string"]);
});
it("should list property names that occur in the file", function () {
it("should list property names that have been declared in the file", function () {
testEditor.setCursorPos({ line: 17, ch: 11 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresent(hintObj, ["propA", "propB", "propC"]);
hintsPresent(hintObj, ["propB"]);
});
it("should list property names that occur in other files", function () {
testEditor.setCursorPos({ line: 17, ch: 11 });
it("should list identifier names that occur in other files", function () {
testEditor.setCursorPos({ line: 16, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresent(hintObj, ["propD", "propE"]);
hintsPresent(hintObj, ["D1", "D2"]);
});
it("should NOT list variable, parameter or function names on property lookups", function () {
@ -398,7 +452,7 @@ define(function (require, exports, module) {
it("should list explicit hints for variable and function names", function () {
testEditor.setCursorPos({ line: 6, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider, null);
hintsPresentExact(hintObj, ["A2", "A3", "funB", "A1"]);
hintsPresent(hintObj, ["A2", "A3", "funB", "A1"]);
});
it("should list implicit hints when typing property lookups", function () {
@ -406,6 +460,8 @@ define(function (require, exports, module) {
expectHints(JSCodeHints.jsHintProvider, ".");
});
/* Single quote and double quote keys cause hasHints() to return false.
It used to return true when string literals were supported.
it("should list implicit hints when typing string literals (single quote)", function () {
testEditor.setCursorPos({ line: 9, ch: 0 });
expectHints(JSCodeHints.jsHintProvider, "'");
@ -415,25 +471,24 @@ define(function (require, exports, module) {
testEditor.setCursorPos({ line: 9, ch: 0 });
expectHints(JSCodeHints.jsHintProvider, "\"");
});
it("should give priority to property names associated with the current context", function () {
testEditor.setCursorPos({ line: 19, ch: 11 });
*/
it("should give priority to identifier names associated with the current context", function () {
testEditor.setCursorPos({ line: 16, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentOrdered(hintObj, ["propB", "propA"]);
hintsPresentOrdered(hintObj, ["propB", "propC"]);
hintsPresentOrdered(hintObj, ["C1", "B1"]);
hintsPresentOrdered(hintObj, ["C2", "B2"]);
});
it("should give priority to property names associated with the current context from other files", function () {
testEditor.setCursorPos({ line: 20, ch: 16 });
testEditor.setCursorPos({ line: 16, ch: 0 });
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentOrdered(hintObj, ["log", "propA"]);
hintsPresentOrdered(hintObj, ["log", "propB"]);
hintsPresentOrdered(hintObj, ["log", "propC"]);
hintsPresentOrdered(hintObj, ["log", "propD"]);
hintsPresentOrdered(hintObj, ["log", "propE"]);
hintsPresentOrdered(hintObj, ["C1", "D1"]);
hintsPresentOrdered(hintObj, ["B1", "D1"]);
hintsPresentOrdered(hintObj, ["A1", "D1"]);
hintsPresentOrdered(hintObj, ["funB", "funE"]);
});
it("should choose the correct delimiter for string literal hints with no query", function () {
/* it("should choose the correct delimiter for string literal hints with no query", function () {
var start = { line: 18, ch: 0 },
end = { line: 18, ch: 18 };
@ -445,17 +500,17 @@ define(function (require, exports, module) {
expect(testDoc.getRange(start, end)).toEqual('"hello\\\\\\" world!"');
});
});
*/
it("should insert value hints with no current query", function () {
var start = { line: 6, ch: 0 },
end = { line: 6, ch: 4 };
end = { line: 6, ch: 2 };
testEditor.setCursorPos(start);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 2); // hint 2 is "funB"
selectHint(JSCodeHints.jsHintProvider, hintObj, "A2"); // hint 2 is "A2"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
expect(testDoc.getRange(start, end)).toEqual("funB");
//expect(testEditor.getCursorPos()).toEqual(end);
expect(testDoc.getRange(start, end)).toEqual("A2");
});
});
@ -466,10 +521,10 @@ define(function (require, exports, module) {
testEditor.setCursorPos(start);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
hintsPresentExact(hintObj, ["A2", "A3", "A1"]);
selectHint(JSCodeHints.jsHintProvider, hintObj, 2); // hint 2 is "A1"
hintsPresent(hintObj, ["A1", "A2", "A3"]);
selectHint(JSCodeHints.jsHintProvider, hintObj, "A1"); // hint 1 is "A1"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
//expect(testEditor.getCursorPos()).toEqual(end);
expect(testDoc.getRange(before, end)).toEqual("A1");
});
});
@ -482,7 +537,7 @@ define(function (require, exports, module) {
testDoc.replaceRange("A1.", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 0); // hint 0 is "propA"
selectHint(JSCodeHints.jsHintProvider, hintObj, "propA"); // hint 0 is "propA"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
@ -499,7 +554,7 @@ define(function (require, exports, module) {
testDoc.replaceRange("A1.prop", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 0); // hint 0 is "propA"
selectHint(JSCodeHints.jsHintProvider, hintObj, "propA"); // hint 0 is "propA"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
@ -516,7 +571,7 @@ define(function (require, exports, module) {
testDoc.replaceRange("A1.pro", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 0); // hint 0 is "propA"
selectHint(JSCodeHints.jsHintProvider, hintObj, "propA"); // hint 0 is "propA"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
expect(testDoc.getRange(start, end)).toEqual("A1.propA");
@ -532,7 +587,7 @@ define(function (require, exports, module) {
testDoc.replaceRange("A1.propB", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 0); // hint 0 is "propA"
selectHint(JSCodeHints.jsHintProvider, hintObj, "propA"); // hint 0 is "propA"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
expect(testDoc.getRange(start, end)).toEqual("A1.propA");
@ -549,7 +604,7 @@ define(function (require, exports, module) {
testDoc.replaceRange("(A1.prop)", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
selectHint(JSCodeHints.jsHintProvider, hintObj, 0); // hint 0 is "propA"
selectHint(JSCodeHints.jsHintProvider, hintObj, "propA"); // hint 0 is "propA"
runs(function () {
expect(testEditor.getCursorPos()).toEqual(end);
@ -558,6 +613,58 @@ define(function (require, exports, module) {
});
});
it("should list hints for string, as string assigned to 's', 's' assigned to 'r' and 'r' assigned to 't'", function () {
var start = { line: 26, ch: 0 },
middle = { line: 26, ch: 2 };
testDoc.replaceRange("t.", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
runs(function () {
hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
});
});
it("should list function type", function () {
var start = { line: 36, ch: 0 },
middle = { line: 36, ch: 5 };
testDoc.replaceRange("funD(", start, start);
testEditor.setCursorPos(middle);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
runs(function () {
hintsPresentExact(hintObj, ["funD(a: string, b: number) -> {x, y}"]);
});
});
it("should list exports from a requirejs module", function () {
var start = { line: 40, ch: 21 };
testEditor.setCursorPos(start);
var hintObj = expectHints(JSCodeHints.jsHintProvider);
runs(function () {
hintsPresentExact(hintObj, ["a", "b", "j"]);
});
});
it("should jump to function", function () {
var start = { line: 43, ch: 0 };
testEditor.setCursorPos(start);
runs(function () {
editorJumped({line: 7, ch: 13});
});
});
it("should jump to var", function () {
var start = { line: 44, ch: 10 };
testEditor.setCursorPos(start);
runs(function () {
editorJumped({line: 3, ch: 6});
});
});
});
});

View File

@ -231,6 +231,7 @@ define({
"CMD_QUICK_OPEN" : "Quick Open",
"CMD_GOTO_LINE" : "Go to Line",
"CMD_GOTO_DEFINITION" : "Go to Definition",
"CMD_JUMPTO_DEFINITION" : "Jump to Definition",
"CMD_JSLINT_FIRST_ERROR" : "Go to First JSLint Error",
"CMD_TOGGLE_QUICK_EDIT" : "Quick Edit",
"CMD_TOGGLE_QUICK_DOCS" : "Quick Docs",