Comparing version 13.1.18 to 13.6.1
@@ -9,5 +9,3 @@ // Map from language file extensions to functions that can autocomplete the | ||
// * ch: the column of the caret, starting with zero. | ||
// - options: Object containing optional parameters: | ||
// * line: String of the current line (which the editor may provide | ||
// more efficiently than the default way. | ||
// - options: Object containing optional parameters. | ||
// | ||
@@ -17,5 +15,3 @@ // Return an object with the following fields: | ||
// - completions: A list of the associated completion to a candidate. | ||
var completer = { | ||
js: jsCompleter | ||
}; | ||
var completer = {}; | ||
@@ -33,31 +29,94 @@ exports = completer; | ||
function Map() { | ||
// Cut off the inheritance tree. | ||
this.map = Object.create(null); | ||
// Firefox landed Maps without forEach, hence the odd check for that. | ||
var Map = this.Map; | ||
if (!(Map && Map.prototype.forEach)) { | ||
var Map = function Map() {}; | ||
Map.prototype = Object.create(null, { | ||
get: { | ||
enumerable: false, | ||
value: function(key) { | ||
return this[key]; | ||
} | ||
}, | ||
has: { | ||
enumerable: false, | ||
value: function(key) { | ||
return this[key] !== undefined; | ||
} | ||
}, | ||
set: { | ||
enumerable: false, | ||
value: function(key, value) { | ||
this[key] = value; | ||
} | ||
}, | ||
delete: { | ||
enumerable: false, | ||
value: function(key) { | ||
if (this.has(key)) { | ||
delete this[key]; | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
}, | ||
forEach: { | ||
enumerable: false, | ||
value: function(callbackfn, thisArg) { | ||
callbackfn = callbackfn.bind(thisArg); | ||
for (var i in this) { | ||
callbackfn(this[i], i, this); | ||
} | ||
} | ||
}, | ||
}); | ||
} | ||
Map.prototype = { | ||
get: function(key) { | ||
return this.map[key]; | ||
// Completion-related data structures. | ||
// | ||
// The only way to distinguish two candidates is through how they are displayed. | ||
// That's how the user can tell the difference, too. | ||
function Candidate(display, postfix, score) { | ||
this.display = display; // What the user sees. | ||
this.postfix = postfix; // What is added when selected. | ||
this.score = score|0; // Its score. | ||
} | ||
function Completion() { | ||
this.candidateFromDisplay = new Map(); | ||
this.candidates = []; | ||
} | ||
Completion.prototype = { | ||
insert: function(candidate) { | ||
this.candidateFromDisplay.set(candidate.display, candidate); | ||
this.candidates.push(candidate); | ||
}, | ||
has: function(key) { | ||
return this.map[key] !== undefined; | ||
}, | ||
set: function(key, value) { | ||
this.map[key] = value; | ||
}, | ||
delete: function(key) { | ||
if (this.has(key)) { | ||
delete this.map[key]; | ||
return true; | ||
} else { | ||
return false; | ||
meld: function(completion) { | ||
for (var i = 0; i < completion.candidates.length; i++) { | ||
var candidate = completion.candidates[i]; | ||
if (!this.candidateFromDisplay.has(candidate.display)) { | ||
// We do not already have this candidate. | ||
this.insert(candidate); | ||
} | ||
} | ||
}, | ||
forEach: function(callbackfn, thisArg) { | ||
callbackfn = callbackfn.bind(thisArg); | ||
for (var i in this.map) { | ||
callbackfn(this.map[i], i, this); | ||
} | ||
sort: function() { | ||
this.candidates.sort(function(a, b) { | ||
// A huge score comes first. | ||
return b.score - a.score; | ||
}); | ||
} | ||
}; | ||
// Shared function: inRange. | ||
// Detect whether an index is within a range. | ||
function inRange(index, range) { | ||
return index > range[0] && index <= range[1]; | ||
} |
// test.js: A module for unit tests. | ||
// Copyright © 2011-2013 Jan Keromnes, Thaddee Tyl. All rights reserved. | ||
// Copyright © 2011-2013 Thaddee Tyl, Jan Keromnes. All rights reserved. | ||
// Code covered by the LGPL license. | ||
@@ -4,0 +4,0 @@ |
540
js/main.js
@@ -0,1 +1,3 @@ | ||
// FIXME: make a constructor to allow a stateful autocompletion engine. | ||
// | ||
@@ -19,17 +21,38 @@ // Get a list of completions we can have, based on the state of the editor. | ||
// - options: Object containing optional parameters: | ||
// * line: String of the current line (which the editor may provide | ||
// more efficiently than the default way. | ||
// * contextFrom: Part of the source necessary to get the context. | ||
// May be a string of the current line (which the editor may provide | ||
// more efficiently than the default way). | ||
// Use this if you know that reduceContext() is too slow for you. | ||
// * global: global object. Can be used to perform level 1 (see above). | ||
// * parse: a JS parser that is compatible with | ||
// https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API | ||
// * parserContinuation: boolean. If true, the parser has a callback argument | ||
// which is called with the AST. | ||
// * tokenize: a JS tokenizer that is compatible with Esprima. | ||
// * fireStaticAnalysis: A Boolean to run the (possibly expensive) static | ||
// analysis. Recommendation: run it at every newline. | ||
// analysis. Recommendation: run it at every change of line. | ||
// // FIXME: put this functionality in a separate method. | ||
// * globalIdentifier: A String to identify the symbol representing the | ||
// JS global object, such as 'window' (the default), for static analysis | ||
// purposes. | ||
// | ||
// Return an object with the following fields: | ||
// - candidates: A list of the matches to a possible completion. | ||
// - completions: A list of the associated completion to a candidate. | ||
// Return a sorted Completion (see entrance/completers.js). | ||
// - candidateFromDisplay: Map from display string to candidate. | ||
// - candidates: A list of candidates: | ||
// * display: a string of what the user sees. | ||
// * postfix: a string of what is added when the user chooses this. | ||
// * score: a number to grade the candidate. | ||
// | ||
function jsCompleter(source, caret, options) { | ||
options = options || {}; | ||
var candidates = []; | ||
var completions = []; | ||
var completion = new Completion(); | ||
// Caching the result of a static analysis for perf purposes. | ||
// Only do this (possibly expensive) operation when required. | ||
if (staticCandidates == null || options.fireStaticAnalysis) { | ||
updateStaticCache(source, caret, | ||
{ parse: options.parse, | ||
parserContinuation: options.parserContinuation }); | ||
} | ||
// We use a primitive sorting algorithm. | ||
@@ -41,37 +64,17 @@ // The candidates are simply concatenated, level after level. | ||
var context = getContext(source, caret); | ||
var context = getContext(options.contextFrom || source, caret, | ||
options.tokenizer); | ||
if (!context) { | ||
// We couldn't get the context, we won't be able to complete. | ||
return completion; | ||
} | ||
// Static analysis (Level 2). | ||
// Only do this (possibly expensive) operation once every new line. | ||
if (staticCandidates == null || options.fireStaticAnalysis) { | ||
staticCandidates = getStaticScope(source, caret) | ||
|| staticCandidates; // If it fails, use the previous version. | ||
if (staticCandidates != null) { | ||
// They have a non-negative score. | ||
var staticCompletion = staticAnalysis(context, | ||
{globalIdentifier: options.globalIdentifier}); | ||
if (!!staticCompletion) { completion.meld(staticCompletion); } | ||
} | ||
var allStaticCandidates = staticCandidates; | ||
// Right now, we can only complete variables. | ||
if ((context.completion === Completion.identifier || | ||
context.completion === Completion.property) && | ||
context.data.length === 1 && allStaticCandidates != null) { | ||
var varName = context.data[0]; | ||
var staticCandidates = []; | ||
allStaticCandidates.forEach(function (value, key) { | ||
var candidate = key; | ||
var weight = value; | ||
// The candidate must match and have something to add! | ||
if (candidate.indexOf(varName) == 0 | ||
&& candidate.length > varName.length) { | ||
staticCandidates.push(candidate); | ||
} | ||
}); | ||
staticCandidates.sort(function(a, b) { | ||
// Sort them according to nearest scope. | ||
return allStaticCandidates.get(b) - allStaticCandidates.get(a); | ||
}); | ||
candidates = candidates.concat(staticCandidates); | ||
completions = completions.concat(staticCandidates | ||
.map(function(candidate) { | ||
return candidate.slice(varName.length); | ||
})); | ||
} | ||
@@ -81,13 +84,5 @@ // Sandbox-based candidates (Level 1). | ||
if (options.global !== undefined) { | ||
// They have a score of -1. | ||
var sandboxCompletion = identifierLookup(options.global, context); | ||
if (sandboxCompletion) { | ||
sandboxCompletion.candidates = sandboxCompletion.candidates | ||
.filter(function(candidate) { | ||
// We are removing candidates from level 2. | ||
if (allStaticCandidates == null) return true; | ||
return !allStaticCandidates.has(candidate); | ||
}); | ||
candidates = candidates.concat(sandboxCompletion.candidates); | ||
completions = completions.concat(sandboxCompletion.completions); | ||
} | ||
if (!!sandboxCompletion) { completion.meld(sandboxCompletion); } | ||
} | ||
@@ -97,48 +92,48 @@ | ||
var keywords = [ | ||
"break", "case", "catch", "class", "continue", "debugger", | ||
"default", "delete", "do", "else", "export", "false", "finally", "for", | ||
"function", "get", "if", "import", "in", "instanceof", "let", "new", | ||
"null", "of", "return", "set", "super", "switch", "this", "true", "throw", | ||
"try", "typeof", "undefined", "var", "void", "while", "with", | ||
]; | ||
// This autocompletion is only meaningful with | ||
if (context.completion === Completion.identifier && | ||
// This autocompletion is only meaningful with identifiers. | ||
if (context.completing === Completing.identifier && | ||
context.data.length === 1) { | ||
for (var i = 0; i < keywords.length; i++) { | ||
var keyword = keywords[i]; | ||
var keywordCompletion = new Completion(); | ||
for (var keyword in JSKeywords) { | ||
// The keyword must match and have something to add! | ||
if (keyword.indexOf(context.data) == 0 | ||
&& keyword.length > context.data.length) { | ||
candidates.push(keyword); | ||
completions.push(keyword.slice(context.data.length)); | ||
if (keyword.indexOf(context.data[0]) == 0 | ||
&& keyword.length > context.data[0].length) { | ||
keywordCompletion.insert(new Candidate( | ||
keyword, | ||
keyword.slice(context.data[0].length), | ||
JSKeywords[keyword])); | ||
// The score depends on the frequency of the keyword. | ||
// See keyword.js | ||
} | ||
} | ||
completion.meld(keywordCompletion); | ||
} | ||
return { | ||
candidates: candidates, | ||
completions: completions, | ||
}; | ||
completion.sort(); | ||
return completion; | ||
} | ||
exports.js = jsCompleter; | ||
// Generic helpers. | ||
// | ||
var esprima = esprima || exports; | ||
// Autocompletion types. | ||
var Completion = { // Examples. | ||
var Completing = { // Examples. | ||
identifier: 0, // foo.ba| | ||
property: 1, // foo.| | ||
string: 2, // "foo".| | ||
regex: 3 // /foo/.| | ||
}; | ||
jsCompleter.Completion = Completion; | ||
jsCompleter.Completing = Completing; | ||
// Fetch data from the position of the caret in a source. | ||
// The data is an object containing the following: | ||
// - completion: a number from the Completion enumeration. | ||
// - completing: a number from the Completing enumeration. | ||
// - data: information about the context. Ideally, a list of strings. | ||
@@ -148,3 +143,3 @@ // | ||
// (with the caret at the end of baz, even if after whitespace) | ||
// will return `{completion:0, data:["foo", "bar", "baz"]}`. | ||
// will return `{completing:0, data:["foo", "bar", "baz"]}`. | ||
// | ||
@@ -156,10 +151,8 @@ // If we cannot get an identifier, returns `null`. | ||
// - caret: an object {line: 0-indexed line, ch: 0-indexed column}. | ||
function getContext(source, caret) { | ||
var tokens = esprima.tokenize(source); | ||
if (tokens[tokens.length - 1].type !== esprima.Token.EOF) { | ||
// If the last token is not an EOF, we didn't tokenize it correctly. | ||
// This special case is handled in case we couldn't tokenize, but the last | ||
// token that *could be tokenized* was an identifier. | ||
return null; | ||
} | ||
function getContext(source, caret, tokenize) { | ||
tokenize = tokenize || esprima.tokenize; | ||
var reduction = reduceContext('' + source, caret); | ||
if (reduction === null) { return null; } | ||
caret = reduction[1]; | ||
var tokens = tokenize(reduction[0], {loc:true}); | ||
@@ -172,15 +165,17 @@ // At this point, we know we were able to tokenize it. | ||
var tokIndex = (tokens.length / 2) | 0; // Truncating to an integer. | ||
var tokIndexPrevValue = tokIndex; | ||
var lastCall = false; | ||
var token; | ||
while (tokIndex !== lowIndex) { | ||
while (lowIndex <= highIndex) { | ||
token = tokens[tokIndex]; | ||
// Note: esprima line numbers start with 1, while caret starts with 0. | ||
if (token.lineNumber - 1 < caret.line) { | ||
lowIndex = tokIndex; | ||
} else if (token.lineNumber - 1 > caret.line) { | ||
if (!token) { return null; } | ||
// Note: The caret is on the first line (as a result of reduceContext). | ||
// Also, Esprima lines start with 1. | ||
if (token.loc.start.line > 1) { | ||
highIndex = tokIndex; | ||
} else if (token.lineNumber - 1 === caret.line) { | ||
} else { | ||
// Now, we need the correct column. | ||
var range = [ | ||
token.range[0] - token.lineStart, | ||
token.range[1] - token.lineStart, | ||
token.loc.start.column, | ||
token.loc.end.column | ||
]; | ||
@@ -193,34 +188,64 @@ if (inRange(caret.ch, range)) { | ||
} else if (range[1] < caret.ch) { | ||
lowIndex = tokIndex; | ||
lowIndex = tokIndex + 1; | ||
} | ||
} | ||
tokIndex = ((highIndex + lowIndex) / 2) | 0; | ||
tokIndex = (highIndex + lowIndex) >>> 1; | ||
if (lastCall) { break; } | ||
if (tokIndex === tokIndexPrevValue) { | ||
tokIndex++; | ||
lastCall = true; | ||
} else { tokIndexPrevValue = tokIndex; } | ||
} | ||
return contextFromToken(tokens, tokIndex, caret); | ||
} | ||
}; | ||
jsCompleter.getContext = getContext; | ||
function inRange(index, range) { | ||
return index > range[0] && index <= range[1]; | ||
} | ||
// Either | ||
// | ||
// { | ||
// completing: Completing.<type of completion>, | ||
// data: <Array of string> | ||
// } | ||
// | ||
// or undefined. | ||
// | ||
// Parameters: | ||
// - tokens: list of tokens. | ||
// - tokIndex: index of the token where the caret is. | ||
// - caret: {line:0, ch:0}, position of the caret. | ||
function contextFromToken(tokens, tokIndex, caret) { | ||
var token = tokens[tokIndex]; | ||
if (token.type === esprima.Token.Punctuator && | ||
token.value === '.') { | ||
// Property completion. | ||
return { | ||
completion: Completion.property, | ||
data: suckIdentifier(tokens, tokIndex, caret) | ||
}; | ||
} else if (token.type === esprima.Token.Identifier) { | ||
var prevToken = tokens[tokIndex - 1]; | ||
if (!token) { return; } | ||
if (token.type === "Punctuator" && token.value === '.') { | ||
if (prevToken) { | ||
if (prevToken.type === "Identifier" || | ||
(prevToken.type === "Keyword" && prevToken.value === "this")) { | ||
// Property completion. | ||
return { | ||
completing: Completing.property, | ||
data: suckIdentifier(tokens, tokIndex, caret) | ||
}; | ||
} else if (prevToken.type === "String") { | ||
// String completion. | ||
return { | ||
completing: Completing.string, | ||
data: [] // No need for data. | ||
}; | ||
} else if (prevToken.type === "RegularExpression") { | ||
// Regex completion. | ||
return { | ||
completing: Completing.regex, | ||
data: [] // No need for data. | ||
}; | ||
} | ||
} | ||
} else if (token.type === "Identifier") { | ||
// Identifier completion. | ||
return { | ||
completion: Completion.identifier, | ||
completing: Completing.identifier, | ||
data: suckIdentifier(tokens, tokIndex, caret) | ||
}; | ||
} else { | ||
return null; | ||
} | ||
} | ||
}; | ||
@@ -236,8 +261,4 @@ // suckIdentifier aggregates the whole identifier into a list of strings, taking | ||
var token = tokens[tokIndex]; | ||
if (token.type === esprima.Token.EOF) { | ||
tokIndex--; | ||
token = tokens[tokIndex]; | ||
} | ||
if (token.type !== esprima.Token.Identifier && | ||
token.type !== esprima.Token.Punctuator) { | ||
if (token.type !== "Identifier" && | ||
token.type !== "Punctuator") { | ||
// Nothing to suck. Nothing to complete. | ||
@@ -249,7 +270,8 @@ return null; | ||
var identifier = []; | ||
while (token.type === esprima.Token.Identifier || | ||
(token.type === esprima.Token.Punctuator && | ||
token.value === '.')) { | ||
if (token.type === esprima.Token.Identifier) { | ||
var endCh = token.range[1] - token.lineStart; | ||
while (token.type === "Identifier" || | ||
(token.type === "Punctuator" && token.value === '.') || | ||
(token.type === "Keyword" && token.value === "this")) { | ||
if (token.type === "Identifier" || | ||
token.type === "Keyword") { | ||
var endCh = token.loc.end.column; | ||
var tokValue; | ||
@@ -271,2 +293,282 @@ if (caret.ch < endCh) { | ||
return identifier; | ||
}; | ||
// Reduce the amount of source code to contextualize, | ||
// and the re-positionned caret in this smaller source code. | ||
// | ||
// For instance, `foo\nfoo.bar.baz|` | ||
// will return `['foo.bar.baz', {line:0, ch:11}]`. | ||
// | ||
// If we cannot get an identifier, returns `null`. | ||
// | ||
// Parameters: | ||
// - source: a string of JS code. | ||
// - caret: an object {line: 0-indexed line, ch: 0-indexed column}. | ||
function reduceContext(source, caret) { | ||
var line = 0; | ||
var column = 0; | ||
var fakeCaret = {line: caret.line, ch: caret.ch}; | ||
// Find the position of the previous line terminator. | ||
var iLT = 0; | ||
var newSpot; | ||
var changedLine = false; | ||
var haveSkipped = false; | ||
var i = 0; | ||
var ch; | ||
var nextch; | ||
while ((line < caret.line | ||
|| (line === caret.line && column < caret.ch)) | ||
&& i < source.length) { | ||
ch = source.charCodeAt(i); | ||
// Count the lines. | ||
if (isLineTerminator(ch)) { | ||
line++; | ||
column = 0; | ||
i++; | ||
iLT = i; | ||
continue; | ||
} else { | ||
column++; | ||
} | ||
if (ch === 34 || ch === 39) { | ||
// Single / double quote: starts a string. | ||
newSpot = skipStringLiteral(source, i, iLT - 1, line, column); | ||
haveSkipped = true; | ||
changedLine = line < newSpot.line; | ||
i = newSpot.index; | ||
line = newSpot.line; | ||
column = newSpot.column; | ||
} else if (ch === 47) { | ||
// Slash. | ||
nextch = source.charCodeAt(i + 1); | ||
prevch = source.charCodeAt(i - 1); | ||
if (nextch === 42 && prevch !== 92) { | ||
// Star: we have a multiline comment. | ||
// Not a backslash before: it isn't in a regex. | ||
newSpot = skipMultilineComment(source, i, line, column); | ||
haveSkipped = true; | ||
changedLine = line < newSpot.line; | ||
i = newSpot.index; | ||
line = newSpot.line; | ||
column = newSpot.column; | ||
} else if (nextch === 47) { | ||
// Two consecutive slashes: we have a single-line comment. | ||
i++; | ||
while (!isLineTerminator(ch) && i < source.length) { | ||
ch = source.charCodeAt(i); | ||
i++; | ||
column++; | ||
} | ||
// `i` is on a line terminator. | ||
i -= 2; | ||
} | ||
} | ||
if (haveSkipped && isLineTerminator(source.charCodeAt(i))) { | ||
haveSkipped = false; | ||
continue; | ||
} | ||
if (changedLine) { | ||
// Have we gone too far? | ||
if (line > caret.line || line === caret.line && column > caret.ch + 1) { | ||
return null; | ||
} else if (line === caret.line) { | ||
iLT = i; | ||
// We need to reset the fake caret's position. | ||
column = 0; | ||
} | ||
changedLine = false; | ||
} else { | ||
i++; | ||
} | ||
} | ||
fakeCaret.line = 0; | ||
fakeCaret.ch = column; | ||
// We can limit tokenization between beginning of line | ||
// to position of the caret. | ||
return [source.slice(iLT, iLT + column + 1), fakeCaret]; | ||
} | ||
// Useful functions stolen from Esprima. | ||
// Invisible characters. | ||
// 7.2 White Space | ||
function isWhiteSpace(ch) { | ||
return (ch === 32) || // space | ||
(ch === 9) || // tab | ||
(ch === 0xB) || | ||
(ch === 0xC) || | ||
(ch === 0xA0) || | ||
(ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0); | ||
} | ||
// 7.3 Line Terminators | ||
function isLineTerminator(ch) { | ||
return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029); | ||
} | ||
// Strings. | ||
// 7.8.4 String Literals | ||
// This Esprima algorithm was heavily modified for my purposes. | ||
// | ||
// Parameters: | ||
// - source: code | ||
// - index: position of the opening quote. | ||
// - indexAtStartOfLine: position of the first character of the current line, | ||
// minus one. | ||
// - lineNumber: starting from 0. | ||
// - column: number. | ||
// | ||
// It returns the following object: | ||
// - index: of the character after the end. | ||
// - line: line number at the end of the string. | ||
// - column: column number of the character after the end. | ||
function skipStringLiteral(source, index, indexAtStartOfLine, | ||
lineNumber, column) { | ||
var quote, ch, code, restore; | ||
var length = source.length; | ||
quote = source[index]; | ||
++index; | ||
while (index < length) { | ||
ch = source[index++]; | ||
if (ch === quote) { | ||
break; | ||
} else if (ch === '\\') { | ||
ch = source[index++]; | ||
if (!ch || !isLineTerminator(ch.charCodeAt(0))) { | ||
switch (ch) { | ||
case 'n': break; | ||
case 'r': break; | ||
case 't': break; | ||
case 'u': | ||
case 'x': | ||
restore = index; | ||
index = scanHexEscape(source, index, ch); | ||
if (index < 0) { | ||
index = restore; | ||
} | ||
break; | ||
case 'b': break; | ||
case 'f': break; | ||
case 'v': break; | ||
default: | ||
if (isOctalDigit(ch)) { | ||
code = '01234567'.indexOf(ch); | ||
if (index < length && isOctalDigit(source[index])) { | ||
code = code * 8 + '01234567'.indexOf(source[index++]); | ||
// 3 digits are only allowed when string starts | ||
// with 0, 1, 2, 3 | ||
if ('0123'.indexOf(ch) >= 0 && | ||
index < length && | ||
isOctalDigit(source[index])) { | ||
code = code * 8 + '01234567'.indexOf(source[index++]); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
} else { | ||
++lineNumber; | ||
if (ch === '\r' && source[index] === '\n') { | ||
++index; | ||
} | ||
indexAtStartOfLine = index; | ||
} | ||
} else if (isLineTerminator(ch.charCodeAt(0))) { | ||
++lineNumber; | ||
indexAtStartOfLine = index; | ||
break; | ||
} | ||
} | ||
return { | ||
index: index, | ||
line: lineNumber, | ||
column: index - indexAtStartOfLine | ||
}; | ||
} | ||
function scanHexEscape(source, index, prefix) { | ||
var i, len, ch, code = 0; | ||
len = (prefix === 'u') ? 4 : 2; | ||
for (i = 0; i < len; ++i) { | ||
if (index < source.length && isHexDigit(source[index])) { | ||
ch = source[index++]; | ||
code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); | ||
} else { | ||
return -1; | ||
} | ||
} | ||
return index; | ||
} | ||
function isOctalDigit(ch) { | ||
return '01234567'.indexOf(ch) >= 0; | ||
} | ||
function isHexDigit(ch) { | ||
return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; | ||
} | ||
// The following function is not from Esprima. | ||
// The index must be positioned in the source on a slash | ||
// that starts a multiline comment. | ||
// | ||
// It returns the following object: | ||
// - index: of the character after the end. | ||
// - line: line number at the end of the comment. | ||
// - column: column number of the character after the end. | ||
function skipMultilineComment(source, index, line, targetLine, column) { | ||
var ch = 47; | ||
while (index < source.length) { | ||
ch = source[index].charCodeAt(0); | ||
if (ch == 42) { | ||
// Current character is a star. | ||
if (index === source.length - 1) { | ||
break; | ||
} | ||
if (source[index + 1].charCodeAt(0) === 47) { | ||
// Next character is a slash. | ||
index += 2; | ||
column += 2; | ||
break; | ||
} | ||
} | ||
index++; | ||
if (isLineTerminator(ch)) { | ||
line++; | ||
column = 0; | ||
} else { | ||
column++; | ||
} | ||
} | ||
return { | ||
index: index, | ||
line: line, | ||
column: column | ||
}; | ||
} |
// Sandbox-based analysis. | ||
// | ||
// Return an object with the following fields: | ||
// - candidates: A list of the matches to a possible completion. | ||
// - completions: A list of the associated completion to a candidate. | ||
// Return a sorted Completion (see entrance/completers.js). | ||
// - candidateFromDisplay: Map from display string to candidate. | ||
// - candidates: A list of candidates: | ||
// * display: a string of what the user sees. | ||
// * postfix: a string of what is added when the user chooses this. | ||
// * score: a number to grade the candidate. | ||
// | ||
@@ -13,30 +16,19 @@ // Parameters: | ||
// See ./main.js. | ||
function identifierLookup(global, context) { | ||
function identifierLookup(global, context, options) { | ||
var matchProp = ''; | ||
var completion = new Completion(); | ||
var value = global; | ||
if (context.completion === Completion.identifier) { | ||
// foo.ba| | ||
for (var i = 0; i < context.data.length - 1; i++) { | ||
var descriptor = getPropertyDescriptor(value, context.data[i]); | ||
if (descriptor.get) { | ||
// This is a getter / setter. | ||
// We might trigger a side-effect by going deeper. | ||
// We must stop before the world blows up in a Michael Bay manner. | ||
value = null; | ||
break; | ||
} else { | ||
// We need to go deeper. One property deeper. | ||
value = value[context.data[i]]; | ||
} | ||
} | ||
if (value != null) { | ||
var symbols; | ||
var store = staticCandidates; | ||
if (context.completing === Completing.identifier || // foo.ba| | ||
context.completing === Completing.property) { // foo.| | ||
symbols = context.data; | ||
if (context.completing === Completing.identifier) { | ||
symbols = context.data.slice(0, -1); | ||
matchProp = context.data[context.data.length - 1]; | ||
} | ||
} else if (context.completion === Completion.property) { | ||
// foo.| | ||
for (var i = 0; i < context.data.length; i++) { | ||
var descriptor = getPropertyDescriptor(value, context.data[i]); | ||
if (descriptor.get) { | ||
for (var i = 0; i < symbols.length; i++) { | ||
var descriptor = getPropertyDescriptor(value, symbols[i]); | ||
if (descriptor && descriptor.get) { | ||
// This is a getter / setter. | ||
@@ -49,19 +41,57 @@ // We might trigger a side-effect by going deeper. | ||
// We need to go deeper. One property deeper. | ||
value = value[context.data[i]]; | ||
value = value[symbols[i]]; | ||
if (value == null) { break; } | ||
} | ||
} | ||
dynAnalysisFromType(completion, symbols, global, matchProp); | ||
} else if (context.completing === Completing.string) { | ||
// "foo".| | ||
value = global.String.prototype; | ||
} else if (context.completing === Completing.regex) { | ||
// /foo/.| | ||
value = global.RegExp.prototype; | ||
} | ||
var result = {candidates: [], completions: []}; | ||
if (value != null) { | ||
var matchedProps = getMatchedProps(value, { matchProp: matchProp }); | ||
result.candidates = Object.keys(matchedProps); | ||
result.completions = result.candidates.map(function (prop) { | ||
return prop.slice(matchProp.length); | ||
completionFromValue(completion, value, matchProp); | ||
} | ||
return completion; | ||
} | ||
// completion: a Completion object, | ||
// symbols: a list of strings of properties. | ||
// global: a JS global object. | ||
// matchProp: the start of the property name to complete. | ||
function dynAnalysisFromType(completion, symbols, global, matchProp) { | ||
var store = staticCandidates; | ||
for (var i = 0; i < symbols.length; i++) { | ||
store = store.properties.get(symbols[i]); | ||
} | ||
// Get the type of this property. | ||
if (!!store) { | ||
store.type.forEach(function(sourceIndices, funcName) { | ||
// The element is an instance of that class (source index = 0). | ||
if (sourceIndices.indexOf(0) >= 0 && global[funcName]) { | ||
completionFromValue(completion, global[funcName].prototype, matchProp); | ||
} | ||
}); | ||
return result; | ||
} | ||
} | ||
} else { | ||
// We cannot give any completion. | ||
return result; // empty result. | ||
// completion: a Completion object, | ||
// value: a JS object | ||
// matchProp: a string of the start of the property to complete. | ||
function completionFromValue(completion, value, matchProp) { | ||
var matchedProps = getMatchedProps(value, { matchProp: matchProp }); | ||
for (var prop in matchedProps) { | ||
// It needs to be a valid property: this is dot completion. | ||
try { | ||
var tokens = esprima.tokenize(prop); | ||
if (tokens.length === 1 && tokens[0].type === "Identifier") { | ||
completion.insert( | ||
new Candidate(prop, prop.slice(matchProp.length), -1)); | ||
} | ||
} catch (e) {} // Definitely not a valid property. | ||
} | ||
@@ -71,3 +101,2 @@ } | ||
// Get all accessible properties on this JS value, as an Object. | ||
@@ -74,0 +103,0 @@ // Filter those properties by name. |
385
js/static.js
@@ -0,7 +1,89 @@ | ||
// Return a Completion instance, or undefined. | ||
// Parameters: | ||
// - context: result of the getContext function. | ||
// - options: | ||
// * globalIdentifier: the string of a global parameter. | ||
// For instance, `global`, or `window` (the default). | ||
function staticAnalysis(context, options) { | ||
options = options || {}; | ||
options.globalIdentifier = options.globalIdentifier || 'window'; | ||
var staticCompletion = new Completion(); | ||
var completingIdentifier = (context.completing === Completing.identifier); | ||
var completingProperty = (context.completing === Completing.property); | ||
var varName; // Each will modify this to the start of the variable name. | ||
var eachProperty = function eachProperty(store, display) { | ||
if (display.indexOf(varName) == 0 | ||
&& display.length > varName.length) { | ||
// The candidate must match and have something to add! | ||
try { | ||
var tokens = esprima.tokenize(display); | ||
if (tokens.length === 1 && tokens[0].type === "Identifier") { | ||
staticCompletion.insert(new Candidate(display, | ||
display.slice(varName.length), store.weight)); | ||
} | ||
} catch (e) {} // Definitely not a valid property. | ||
} | ||
}; | ||
if (completingIdentifier && context.data.length === 1) { | ||
varName = context.data[0]; | ||
// They have a positive score. | ||
staticCandidates.properties.forEach(eachProperty); | ||
if (options.globalIdentifier && | ||
staticCandidates.properties[options.globalIdentifier]) { | ||
// Add properties like `window.|`. | ||
staticCandidates.properties[options.globalIdentifier].properties | ||
.forEach(eachProperty); | ||
} | ||
} else if (completingIdentifier || completingProperty) { | ||
var store = staticCandidates; | ||
for (var i = 0; i < context.data.length - 1; i++) { | ||
store = store.properties.get(context.data[i]); | ||
if (!store) { return; } | ||
} | ||
varName = context.data[i]; | ||
if (completingProperty) { | ||
store = store.properties.get(varName); | ||
if (!store) { return; } | ||
varName = ''; // This will cause the indexOf check to succeed. | ||
} | ||
store.properties.forEach(eachProperty); | ||
// Seek data from its type. | ||
if (!!store.type) { | ||
store.type.forEach(function(sourceIndices, funcName) { | ||
funcStore = staticCandidates.properties.get(funcName); | ||
if (!funcStore) { return; } | ||
for (var i = 0; i < store.type[funcName].length; i++) { | ||
var sourceIndex = store.type[funcName][i]; | ||
// Each sourceIndex corresponds to a source, | ||
// and the `sources` property is that source. | ||
if (funcStore.sources) { | ||
funcStore.sources[sourceIndex].forEach(eachProperty); | ||
if (sourceIndex === 0) { | ||
// This was a constructor. | ||
var protostore = funcStore.properties.get('prototype'); | ||
if (!protostore) { return; } | ||
protostore.properties.forEach(eachProperty); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
return staticCompletion; | ||
} | ||
// Static analysis helper functions. | ||
// | ||
// Cache in use for static analysis. | ||
var staticCandidates; // We keep the previous candidates around. | ||
// | ||
@@ -12,5 +94,3 @@ // Get all the variables in a JS script at a certain position. | ||
// | ||
// Returns a map from all variable names to a number reflecting how deeply | ||
// nested in the scope the variable was. A bigger number reflects a more | ||
// deeply nested variable. | ||
// Returns a TypeStore object. See below. | ||
// We return null if we could not parse the code. | ||
@@ -23,18 +103,37 @@ // | ||
// - source: The JS script to parse. | ||
// - caret: {line:0, ch:0} The line and column in the script from which we want the scope. | ||
// - store: | ||
// (Optional) The object we return. Use to avoid allocation. | ||
// - depth: | ||
// (Optional, defaults to 0.) A starting point for indicating how deeply | ||
// nested variables are. | ||
// - caret: {line:0, ch:0} The line and column in the scrip | ||
// from which we want the scope. | ||
// - options: | ||
// * store: The object we return. Use to avoid allocation. | ||
// It is a TypeStore. | ||
// * parse: A JS parser that conforms to | ||
// https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API | ||
// * parserContinuation: A boolean. If true, the parser has a callback | ||
// argument that sends the AST. | ||
// | ||
function getStaticScope(source, caret, store, depth) { | ||
store = store || new Map(); | ||
depth = depth || 0; | ||
function updateStaticCache(source, caret, options) { | ||
options = options || {}; | ||
options.store = options.store || new TypeStore(); | ||
options.parse = options.parse || esprima.parse; | ||
var tree; | ||
try { | ||
tree = esprima.parse(source, {loc:true}); | ||
if (!!options.parserContinuation) { | ||
options.parse(source, {loc:true}, function(tree) { | ||
staticCandidates = getStaticScope(tree, caret, options) | ||
|| staticCandidates; // If it fails, use the previous version. | ||
}); | ||
} else { | ||
var tree = options.parse(source, {loc:true}); | ||
staticCandidates = getStaticScope(tree, caret, options) | ||
|| staticCandidates; // If it fails, use the previous version. | ||
} | ||
} catch (e) { return null; } | ||
} | ||
jsCompleter.updateStaticCache = updateStaticCache; | ||
function getStaticScope(tree, caret, options) { | ||
var subnode, symbols; | ||
var store = options.store; | ||
var node = tree.body; | ||
@@ -48,3 +147,3 @@ var stack = []; | ||
for (; index < node.length; index++) { | ||
var subnode = node[index]; | ||
subnode = node[index]; | ||
while (["ReturnStatement", "VariableDeclarator", "ExpressionStatement", | ||
@@ -57,3 +156,19 @@ "AssignmentExpression", "Property"].indexOf(subnode.type) >= 0) { | ||
// Variable names go one level too deep. | ||
store.set(subnode.id.name, stack.length - 1); | ||
if (subnode.init && subnode.init.type === "NewExpression") { | ||
store.addProperty(subnode.id.name, // property name | ||
{ name: subnode.init.callee.name, // atomic type | ||
index: 0 }, // created from `new C()` | ||
stack.length - 1); // weight | ||
store.addProperty(subnode.init.callee.name, | ||
{ name: 'Function', index: 0 }); | ||
// FIXME: add built-in types detection. | ||
} else if (subnode.init && subnode.init.type === "Literal" || | ||
subnode.init && subnode.init.type === "ObjectExpression" || | ||
subnode.init && subnode.init.type === "ArrayExpression") { | ||
typeFromLiteral(store, [subnode.id.name], subnode.init); | ||
store.properties.get(subnode.id.name).weight = stack.length - 1; | ||
} else { | ||
// Simple object. | ||
store.addProperty(subnode.id.name, null, stack.length - 1); | ||
} | ||
if (!!subnode.init) { | ||
@@ -68,4 +183,19 @@ subnode = subnode.init; | ||
if (subnode.type == "AssignmentExpression") { | ||
if (subnode.left.type === "MemberExpression") { | ||
symbols = typeFromMember(store, subnode.left); | ||
} | ||
if (subnode.right.type === "ObjectExpression") { | ||
typeFromLiteral(store, symbols, subnode.right); | ||
} | ||
subnode = subnode.right; // f.g = function(){…}; | ||
} | ||
if (subnode.type == "CallExpression") { | ||
if (subnode.callee.name) { // f() | ||
store.addProperty(subnode.callee.name, | ||
{ name: 'Function', index: 0 }, | ||
stack.length); | ||
} else if (!subnode.callee.body) { // f.g() | ||
typeFromMember(store, subnode.callee); | ||
} | ||
} | ||
if (subnode.type == "Property") { | ||
@@ -83,3 +213,6 @@ subnode = subnode.value; // {f: function(){…}}; | ||
if (subnode.id) { | ||
store.set(subnode.id.name, stack.length); | ||
store.addProperty(subnode.id.name, | ||
{ name: 'Function', index: 0 }, | ||
stack.length); | ||
readThisProps(store, subnode); | ||
} | ||
@@ -130,5 +263,3 @@ if (caretInBlock(subnode, caret)) { | ||
} else if (node.consequent) { | ||
body = node.consequent.body; // If statements. | ||
} else if (node.alternate) { | ||
body = node.alternate.body; // If/else statements. | ||
body = fakeIfNodeList(node); // If statements. | ||
} else if (node.block) { | ||
@@ -159,2 +290,20 @@ body = node.block.body; // Try statements. | ||
// | ||
// Construct a list of nodes to go through based on the sequence of ifs and else | ||
// ifs and elses. | ||
// | ||
// Parameters: | ||
// - node: an AST node of type IfStatement. | ||
function fakeIfNodeList(node) { | ||
var body = [node.consequent]; | ||
if (node.alternate) { | ||
if (node.alternate.type === "IfStatement") { | ||
body = body.concat(fakeIfNodeList(node.alternate)); | ||
} else if (node.alternate.type === "BlockStatement") { | ||
body.push(node.alternate); | ||
} | ||
} | ||
return body; | ||
} | ||
// | ||
// Whether the caret is in the piece of code represented by the node. | ||
@@ -193,5 +342,199 @@ // | ||
for (var i = 0; i < node.length; i++) { | ||
store.set(node[i].name, weight); | ||
store.addProperty(node[i].name, null, weight); | ||
} | ||
} | ||
// | ||
// Type inference. | ||
// A type is a list of sources. | ||
// | ||
// *Sources* can be either: | ||
// | ||
// - The result of a `new Constructor()` call. | ||
// - The result of a function. | ||
// - A parameter to a function. | ||
// | ||
// Each function stores information in the TypeStore about all possible sources | ||
// it can give, as a list of sources (aka maps to typestores): | ||
// | ||
// [`this` properties, return properties, param1, param2, etc.] | ||
// | ||
// Each instance stores information about the list of sources it may come from. | ||
// Inferred information about the properties of each instance comes from the | ||
// aggregated properties of each source. | ||
// The type is therefore a map of the following form. | ||
// | ||
// { "name of the original function": [list of indices of source] } | ||
// | ||
// We may represent atomic type outside a compound type as the following: | ||
// | ||
// { name: "name of the origin", index: source index } | ||
// | ||
// A type inference instance maps symbols to an object of the following form: | ||
// - properties: a Map from property symbols to typeStores for its properties, | ||
// - type: a structural type (ie, not atomic) (see above). | ||
// - weight: integer, relevance of the symbol, | ||
function TypeStore(type, weight) { | ||
this.properties = new Map(); | ||
this.type = type || new Map(); | ||
this.weight = weight|0; | ||
if (this.type.has("Function")) { | ||
// The sources for properties on `this` and on the return object. | ||
this.sources = [new Map(), new Map()]; | ||
} | ||
} | ||
TypeStore.prototype = { | ||
// Add a property named `symbol` typed from the atomic type `atype`. | ||
// `atype` and `weight` may not be present. | ||
addProperty: function(symbol, atype, weight) { | ||
if (!this.properties.has(symbol)) { | ||
if (atype != null) { | ||
var newType = new Map(); | ||
var typeSources = [atype.index]; | ||
newType.set(atype.name, typeSources); | ||
} | ||
this.properties.set(symbol, new TypeStore(newType, weight)); | ||
} else { | ||
// The weight is proportional to the frequency. | ||
var p = this.properties.get(symbol); | ||
p.weight++; // FIXME: this increment is questionnable. | ||
if (atype != null) { | ||
p.addType(atype); | ||
} | ||
} | ||
}, | ||
// Given an atomic type (name, index), is this one? | ||
hasType: function(atype) { | ||
if (!this.type.has(atype.name)) { return false; } | ||
return this.type.get(atype.name).indexOf(atype.index) >= 0; | ||
}, | ||
// We can add an atomic type (a combination of the name of the original | ||
// function and the source index) to an existing compound type. | ||
addType: function(atype) { | ||
if (atype.name === "Function") { | ||
// The sources for properties on `this` and on the return object. | ||
this.sources = this.sources || [new Map(), new Map()]; | ||
} | ||
if (this.type.has(atype.name)) { | ||
// The original function name is already known. | ||
var sourceIndices = this.type.get(atype.name); | ||
if (sourceIndices.indexOf(atype.index) === -1) { | ||
sourceIndices.push(atype.index); | ||
} | ||
} else { | ||
// New original function name (common case). | ||
var sourceIndices = []; | ||
sourceIndices.push(atype.index); | ||
this.type.set(atype.name, sourceIndices); | ||
} | ||
} | ||
}; | ||
// Store is a TypeStore instance, | ||
// node is a MemberExpression. | ||
// funName is the name of the containing function. | ||
// Having funName set prevents setting properties on `this`. | ||
function typeFromMember(store, node, funName) { | ||
var symbols, symbol, i; | ||
symbols = []; | ||
symbol = ''; | ||
while (node.object && // `foo()` doesn't have a `.object`. | ||
node.object.type !== "Identifier" && | ||
node.object.type !== "ThisExpression") { | ||
symbols.push(node.property.name); | ||
node = node.object; | ||
} | ||
symbols.push(node.property.name); | ||
if (node.object.type !== "ThisExpression") { | ||
symbols.push(node.object.name); // At this point, node is an identifier. | ||
} else { | ||
// Add the `this` properties to the function's generic properties. | ||
var func = store.properties.get(funName); | ||
if (!!func) { | ||
for (i = symbols.length - 1; i >= 0; i--) { | ||
symbol = symbols[i]; | ||
func.sources[0].set(symbol, new TypeStore()); | ||
func = func.properties.get(symbol); | ||
} | ||
return symbols; | ||
} else if (!!funName) { | ||
// Even if we don't have a function, we must stop there | ||
// if funName is defined. | ||
return symbols; | ||
} | ||
// Treat `this` as a variable inside the function. | ||
symbols.push("this"); | ||
} | ||
// Now that we have the symbols, put them in the store. | ||
symbols.reverse(); | ||
for (i = 0; i < symbols.length; i++) { | ||
symbol = symbols[i]; | ||
store.addProperty(symbol); | ||
store = store.properties.get(symbol); | ||
} | ||
return symbols; | ||
} | ||
// Store is a TypeStore instance, | ||
// node is a Literal or an ObjectExpression. | ||
function typeFromLiteral(store, symbols, node) { | ||
var property, i, substore, nextSubstore; | ||
substore = store; | ||
// Find the substore insertion point. | ||
for (i = 0; i < symbols.length; i++) { | ||
nextSubstore = substore.properties.get(symbols[i]); | ||
if (!nextSubstore) { | ||
// It really should exist. | ||
substore.addProperty(symbols[i]); | ||
nextSubstore = substore.properties.get(symbols[i]); | ||
} | ||
substore = nextSubstore; | ||
} | ||
// Add the symbols. | ||
var constructor = "Object"; | ||
if (node.type === "ObjectExpression") { | ||
for (i = 0; i < node.properties.length; i++) { | ||
property = node.properties[i]; | ||
var propname = property.key.name? property.key.name | ||
: property.key.value; | ||
substore.addProperty(propname); | ||
if (property.value.type === "ObjectExpression") { | ||
// We can recursively complete the object tree. | ||
typeFromLiteral(store, symbols.concat(propname), property.value); | ||
} | ||
} | ||
} else if (node.type === "ArrayExpression") { | ||
constructor = 'Array'; | ||
} else if (node.value instanceof RegExp) { | ||
constructor = 'RegExp'; | ||
} else if (typeof node.value === "number") { | ||
constructor = 'Number'; | ||
} else if (typeof node.value === "string") { | ||
constructor = 'String'; | ||
} else if (typeof node.value === "boolean") { | ||
constructor = 'Boolean'; | ||
} | ||
substore.addType({ name: constructor, index: 0 }); | ||
} | ||
// Assumes that the function has an explicit name. | ||
function readThisProps(store, node) { | ||
var funcStore = store.properties.get(node.id.name); | ||
var statements = node.body.body; | ||
var i = 0; | ||
for (; i < statements.length; i++) { | ||
if (statements[i].expression && | ||
statements[i].expression.type === "AssignmentExpression" && | ||
statements[i].expression.left.type === "MemberExpression") { | ||
typeFromMember(store, statements[i].expression.left, node.id.name); | ||
} | ||
} | ||
} |
202
js/test.js
@@ -9,9 +9,12 @@ // Testing files in this directory. | ||
var source; | ||
var caret; | ||
// Testing main.js | ||
// getContext(source, caret) | ||
var source = 'var foo.bar;baz'; | ||
var caret = {line:0, ch:15}; | ||
source = 'var foo.bar;baz'; | ||
caret = {line:0, ch:15}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completion: jsCompleter.Completion.identifier, | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['baz'] }, | ||
@@ -21,3 +24,3 @@ 'getContext cares for semi-colons.'); | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completion: jsCompleter.Completion.identifier, | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['foo', 'bar'] }, | ||
@@ -27,10 +30,44 @@ "getContext takes all identifiers (doesn't stop with a dot)."); | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completion: jsCompleter.Completion.identifier, | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['foo', 'ba'] }, | ||
"getContext cuts identifiers on the cursor."); | ||
source = 'var foo.bar;\nbaz'; | ||
caret = {line:1, ch:3}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['baz'] }, | ||
"getContext deals with multiple lines."); | ||
source = 'var foo/*.bar;\n bar*/ baz'; | ||
caret = {line:1, ch:10}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['baz'] }, | ||
"getContext deals with multiple line comments."); | ||
source = 'var foo "bar\\\n bar" baz'; | ||
caret = {line:1, ch:9}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['baz'] }, | ||
"getContext deals with multiple line strings."); | ||
source = 'var foo "bar\\\n bar'; | ||
caret = {line:1, ch:4}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
undefined, | ||
"getContext deals with untokenizable contexts."); | ||
source = 'this.foo'; | ||
caret = {line:0, ch:8}; | ||
t.eq(jsCompleter.getContext(source, caret), | ||
{ completing: jsCompleter.Completing.identifier, | ||
data: ['this', 'foo'] }, | ||
"getContext deals with `this`."); | ||
// Testing sandbox.js | ||
var source = 'foo.ba'; | ||
var caret = {line:0, ch:source.length}; | ||
source = 'foo.ba'; | ||
caret = {line:0, ch:source.length}; | ||
// Create a global object with no inheritance. | ||
@@ -40,15 +77,154 @@ var global = Object.create(null); | ||
global.foo.bar = 0; | ||
t.eq(jsCompleter(source, caret, {global:global}), | ||
{candidates:['bar'], completions:['r']}, | ||
t.eq(jsCompleter(source, caret, {global:global}).candidates, | ||
[{display:"bar", postfix:"r", score:-1}], | ||
"The JS completer works with dynamic analysis."); | ||
source = '"foo".'; | ||
caret = {line:0, ch:source.length}; | ||
// Fake String.prototype. | ||
global.String = Object.create(null); | ||
global.String.prototype = Object.create(null); | ||
global.String.prototype.big = 1; | ||
t.eq(jsCompleter(source, caret, {global:global}).candidates, | ||
[{display:"big", postfix:"big", score:-1}], | ||
"The JS completer does string completion."); | ||
source = '/foo/.'; | ||
caret = {line:0, ch:source.length}; | ||
// Fake String.prototype. | ||
global.RegExp = Object.create(null); | ||
global.RegExp.prototype = Object.create(null); | ||
global.RegExp.prototype.test = 'something'; | ||
t.eq(jsCompleter(source, caret, {global:global}).candidates, | ||
[{display:"test", postfix:"test", score:-1}], | ||
"The JS completer does RegExp completion."); | ||
// Testing static.js | ||
var source = 'var foobar; foo'; | ||
var caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}), | ||
{candidates:['foobar'], completions:['bar']}, | ||
source = 'var foobar; foo'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"foobar", postfix:"bar", score:0}], | ||
"The JS completer works with static analysis."); | ||
source = 'foo.bar = 5; foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The JS completer has static object analysis in property assignments."); | ||
source = 'foo.bar(); foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The JS completer has static object analysis in function calls."); | ||
source = 'foo.bar = {baz:5}; foo.bar.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"baz", postfix:"az", score:0}], | ||
"The JS completer has static object literal analysis " + | ||
"in object assignments."); | ||
source = 'var foo = {bar:5}; foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The JS completer has static object analysis in definition."); | ||
source = 'var foo = {"bar":5}; foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The JS completer has static object analysis even with strings."); | ||
source = 'this.bar = 5; this.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The JS completer has static object analysis even with strings."); | ||
source = 'F.prototype.bar = 0; var foo = new F(); foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The static analysis goes through the prototype."); | ||
source = 'var foo = {"b*": 0}; foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[], | ||
"The static analysis doesn't complete non-identifiers."); | ||
source = 'var foo = {b: {bar: 0}}; foo.b.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The static analysis sees types in objects."); | ||
source = 'if (foo) {} else if (foo) {foo.bar = function () { foo.b }}'; | ||
caret = {line:0, ch:source.length - 3}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"The static analysis goes through else clauses."); | ||
source = 'f.foo = {bar: 0}; f.foo.b'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates, | ||
[{display:"bar", postfix:"ar", score:0}], | ||
"Static analysis with assignment to property."); | ||
source = 'var foo = 0; foo.b'; | ||
global = Object.create(null); | ||
global.Number = Object.create(null); | ||
global.Number.prototype = Object.create(null); | ||
global.Number.prototype.bar = 0; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, | ||
{fireStaticAnalysis:true, global:global}).candidates, | ||
[{display:"bar", postfix:"ar", score:-1}], | ||
"Static analysis maps literals to built-in types."); | ||
source = 'var foo = []; foo.b'; | ||
global = Object.create(null); | ||
global.Array = Object.create(null); | ||
global.Array.prototype = Object.create(null); | ||
global.Array.prototype.bar = 0; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, | ||
{fireStaticAnalysis:true, global:global}).candidates, | ||
[{display:"bar", postfix:"ar", score:-1}], | ||
"Static analysis identifies array literals."); | ||
source = 'window.quux = 0; qu'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, | ||
{fireStaticAnalysis:true, globalIdentifier:'window'}).candidates, | ||
[{display:"quux", postfix:"ux", score:0}], | ||
"Static analysis uses the globalIdentifier option."); | ||
source = 'init(); ini'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, | ||
{fireStaticAnalysis:true}).candidates, | ||
[{display:"init", postfix:"t", score:0}], | ||
"Static analysis reads undefined functions."); | ||
// Testing keyword completion | ||
source = 'vo'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates[0].display, | ||
"void", | ||
"The JS completer knows keywords (or at least 'void')."); | ||
source = 'vo'; | ||
caret = {line:0, ch:source.length}; | ||
t.eq(jsCompleter(source, caret, {fireStaticAnalysis:true}).candidates[0].postfix, | ||
"id", | ||
"The JS completer completes keywords (or at least 'void')."); | ||
// The End. | ||
@@ -55,0 +231,0 @@ |
27
make.js
@@ -25,2 +25,8 @@ // Contrary to popular belief, this file is meant to be a JS code concatenator. | ||
// Web workers for static analysis. | ||
bundle('demo/parser-worker.js', [ | ||
'node_modules/esprima/esprima.js', | ||
'js/worker-parser.js', | ||
]); | ||
// Target environment: AMD / Node.js / plain old browsers. | ||
@@ -30,7 +36,24 @@ // | ||
'entrance/umd-begin.js', | ||
'entrance/completers.js', | ||
// JS completion files. | ||
'entrance/compl-begin.js', | ||
'js/main.js', | ||
'js/static.js', | ||
'js/sandbox.js', | ||
'js/main.js', | ||
'entrance/completers.js', | ||
'js/keyword.js', | ||
'entrance/compl-end.js', | ||
// CSS completion files. | ||
'entrance/compl-begin.js', | ||
'css/main.js', | ||
// Import tokenizer (with export shim). | ||
'css/css-token-begin.js', | ||
'css/tokenizer.js', | ||
'css/css-token-end.js', | ||
// Properties. | ||
'css/properties.js', | ||
'entrance/compl-end.js', | ||
'entrance/umd-end.js', | ||
]); |
{ | ||
"name": "aulx", | ||
"description": "Autocompletion for the Web. Includes JS with static and dynamic analysis.", | ||
"author": "Thaddee Tyl <thaddee.tyl@gmail.com> (http://espadrine.github.com/)", | ||
@@ -6,5 +8,4 @@ "contributors": [ | ||
], | ||
"name": "aulx", | ||
"description": "Autocompletion for the Web. Includes JS with static and dynamic analysis.", | ||
"version": "13.01.18", | ||
"main": "aulx.js", | ||
"version": "13.06.01", | ||
"homepage": "http://espadrine.github.com/aulx/", | ||
@@ -15,20 +16,14 @@ "repository": { | ||
}, | ||
"main": "lib/camp.js", | ||
"scripts": { | ||
"test": "make test" | ||
}, | ||
"engines": { | ||
"node": ">=0.4.0" | ||
}, | ||
"dependencies": { | ||
"socket.io": "0.9.2", | ||
"formidable": "1.0.11" | ||
"esprima": "1.0.2" | ||
}, | ||
"devDependencies": {}, | ||
"optionalDependencies": {}, | ||
"engines": { | ||
"node": "*" | ||
}, | ||
"directories": { | ||
"lib": "./lib", | ||
"doc": "./doc", | ||
"examples": "node app 1234" | ||
} | ||
"readmeFilename": "Readme.md" | ||
} |
@@ -25,10 +25,9 @@ # Aulx [![Build Status](https://travis-ci.org/espadrine/aulx.png)](https://travis-ci.org/espadrine/aulx) | ||
- JS static analysis: a simple algorithm for autocompletion, | ||
- JS dynamic analysis. | ||
- JS Static type inference, | ||
- JS dynamic analysis, | ||
- CSS property autocompletion. | ||
To do: | ||
- A better module system, | ||
- Better static analysis for JS, | ||
- CSS, | ||
- HTML, | ||
- HTML (including inlined CSS and JS autocompletion), | ||
- CoffeeScript, SASS, … We can go crazy with this! | ||
@@ -42,7 +41,6 @@ | ||
The main dev entry point is at `entrance/completers.js`. | ||
Project entry point: `entrance/completers.js`. | ||
It uses all completers, each of which has its own directory. | ||
The main entry point for each of those folder is, quite unexpectedly, `main.js`. | ||
They also all have a `test.js` file, which is used for testing. | ||
Completer entry point: `<completer>/main.js` (no surprise there!) | ||
@@ -57,4 +55,4 @@ Building the bundle `aulx.js` is done with this swift command: | ||
Finally, testing completers is either done in batch mode with this other swift | ||
command: | ||
Finally, testing completers is either done in batch mode with yet another | ||
swift command: | ||
@@ -69,1 +67,8 @@ make test | ||
---- | ||
*Baked by Thaddée Tyl*. | ||
This work is licensed under the Creative Commons Attribution 3.0 Unported | ||
License. To view a copy of this license, visit | ||
http://creativecommons.org/licenses/by/3.0/. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
784654
1
37
13899
71
3
+ Addedesprima@1.0.2
+ Addedesprima@1.0.2(transitive)
- Removedformidable@1.0.11
- Removedsocket.io@0.9.2
- Removedactive-x-obfuscator@0.0.1(transitive)
- Removedcommander@2.1.0(transitive)
- Removedformidable@1.0.11(transitive)
- Removednan@1.0.0(transitive)
- Removedoptions@0.0.6(transitive)
- Removedpolicyfile@0.0.4(transitive)
- Removedredis@0.6.7(transitive)
- Removedsocket.io@0.9.2(transitive)
- Removedsocket.io-client@0.9.2(transitive)
- Removedtinycolor@0.0.1(transitive)
- Removeduglify-js@1.2.5(transitive)
- Removedws@0.4.32(transitive)
- Removedxmlhttprequest@1.2.2(transitive)
- Removedzeparser@0.0.5(transitive)