c9-core/plugins/c9.ide.language.core/worker.js

1923 wiersze
74 KiB
JavaScript

/**
* Cloud9 Language Foundation
*
* @copyright 2013, Ajax.org B.V.
*/
/**
* Language Worker
* This code runs in a WebWorker in the browser. Its main job is to
* delegate messages it receives to the various handlers that have registered
* themselves with the worker.
*/
define(function(require, exports, module) {
// TODO fix build script to check for deps in ace/worker/worker.js
require("ace/lib/es6-shim");
var oop = require("ace/lib/oop");
var Mirror = require("ace/worker/mirror").Mirror;
var tree = require('treehugger/tree');
var EventEmitter = require("ace/lib/event_emitter").EventEmitter;
var syntaxDetector = require("plugins/c9.ide.language.core/syntax_detector");
var completeUtil = require("plugins/c9.ide.language/complete_util");
var localCompleter = require("plugins/c9.ide.language.generic/local_completer");
var openFilesCompleter = require("plugins/c9.ide.language.generic/open_files_local_completer");
var base_handler = require("plugins/c9.ide.language/base_handler");
var assert = require("c9/assert");
var isInWebWorker = typeof window == "undefined" || !window.location || !window.document;
var WARNING_LEVELS = {
error: 3,
warning: 2,
info: 1
};
var UPDATE_TIMEOUT_MIN = !isInWebWorker && window.c9Test ? 5 : 200;
var UPDATE_TIMEOUT_MAX = 15000;
var DEBUG = !isInWebWorker; // set to true by setDebug() for c9.dev/cloud9beta.com
var STATS = false;
// Leaking into global namespace of worker, to allow handlers to have access
/*global disabledFeatures: true*/
disabledFeatures = {};
var ServerProxy = function(sender) {
this.emitter = Object.create(EventEmitter);
this.emitter.emit = this.emitter._dispatchEvent;
this.send = function(data) {
sender.emit("serverProxy", data);
};
this.once = function(messageType, messageSubtype, callback) {
var channel = messageType;
if (messageSubtype)
channel += (":" + messageSubtype);
this.emitter.once(channel, callback);
};
this.subscribe = function(messageType, messageSubtype, callback) {
var channel = messageType;
if (messageSubtype)
channel += (":" + messageSubtype);
this.emitter.addEventListener(channel, callback);
};
this.unsubscribe = function(messageType, messageSubtype, f) {
var channel = messageType;
if (messageSubtype)
channel += (":" + messageSubtype);
this.emitter.removeEventListener(channel, f);
};
this.onMessage = function(msg) {
var channel = msg.type;
if (msg.subtype)
channel += (":" + msg.subtype);
// console.log("publish to: " + channel);
this.emitter.emit(channel, msg.body);
};
};
exports.createUIWorkerClient = function() {
var emitter = Object.create(require("ace/lib/event_emitter").EventEmitter);
var result = new LanguageWorker(emitter);
result.on = function(name, f) {
emitter.on.call(result, name, f);
};
result.once = function(name, f) {
emitter.once.call(result, name, f);
};
result.removeEventListener = function(f) {
emitter.removeEventListener.call(result, f);
};
result.call = function(cmd, args, callback) {
if (callback) {
var id = this.callbackId++;
this.callbacks[id] = callback;
args.push(id);
}
this.send(cmd, args);
};
result.send = function(cmd, args) {
setTimeout(function() { result[cmd].apply(result, args); }, 0);
};
result.emit = function(event, data) {
emitter._dispatchEvent.call(emitter, event, data);
};
emitter.emit = function(event, data) {
emitter._dispatchEvent.call(result, event, { data: data });
};
result.changeListener = function(e) {
this.emit("change", { data: [e.data]});
};
return result;
};
var LanguageWorker = exports.LanguageWorker = function(sender) {
var _self = this;
this.$keys = {};
this.handlers = [];
this.$warningLevel = "info";
this.$openDocuments = {};
this.$initedRegexes = {};
this.lastUpdateTime = 0;
sender.once = EventEmitter.once;
this.serverProxy = new ServerProxy(sender);
Mirror.call(this, sender);
this.setTimeout(0);
exports.sender = sender;
exports.$lastWorker = this;
sender.on("hierarchy", function(event) {
_self.hierarchy(event);
});
sender.on("code_format", function(event) {
_self.codeFormat();
});
sender.on("outline", applyEventOnce(function(event) {
_self.outline(event);
}));
sender.on("complete", applyEventOnce(function(data) {
_self.complete(data);
}), true);
sender.on("documentClose", function(event) {
_self.documentClose(event);
});
sender.on("analyze", applyEventOnce(function(event) {
_self.analyze(false, function() {});
}));
sender.on("cursormove", function(event) {
_self.onCursorMove(event);
});
sender.on("inspect", applyEventOnce(function(event) {
_self.inspect(event);
}));
sender.on("jumpToDefinition", applyEventOnce(function(event) {
_self.jumpToDefinition(event);
}));
sender.on("quickfixes", applyEventOnce(function(event) {
_self.quickfix(event);
}));
sender.on("isJumpToDefinitionAvailable", applyEventOnce(function(event) {
_self.isJumpToDefinitionAvailable(event);
}));
sender.on("refactorings", function(event) {
_self.getRefactorings(event);
});
sender.on("renamePositions", function(event) {
_self.getRenamePositions(event);
});
sender.on("onRenameBegin", function(event) {
_self.onRenameBegin(event);
});
sender.on("commitRename", function(event) {
_self.commitRename(event);
});
sender.on("onRenameCancel", function(event) {
_self.onRenameCancel(event);
});
sender.on("serverProxy", function(event) {
_self.serverProxy.onMessage(event.data);
});
sender.on("quickfix_key", function(e) {
_self.$keys.quickfix = e.data;
});
};
/**
* Ensure that an event handler is called only once if multiple
* events are received at roughly the same time.
**/
function applyEventOnce(eventHandler, waitForMirror) {
var timer;
var mirror = this;
return function(e) {
var _arguments = [].slice.apply(arguments);
if (timer && !(e && e.data.predictOnly))
clearTimeout(timer);
timer = setTimeout(function() {
if (waitForMirror && mirror.isPending())
return setTimeout(function() { applyEventOnce(eventHandler, true); }, 0);
eventHandler.apply(eventHandler, _arguments);
}, 0);
};
}
oop.inherits(LanguageWorker, Mirror);
var asyncForEach = module.exports.asyncForEach = function(array, fn, test, callback) {
if (!callback) {
callback = test;
test = null;
}
array = array.slice(); // copy before use
var nested = false, callNext = true;
loop();
function loop() {
while (callNext && !nested) {
callNext = false;
while (array.length > 0 && test && !test(array[0]))
array.shift();
var item = array.shift();
// TODO: implement proper err argument?
if (!item)
return callback && callback();
nested = true;
fn(item, loop);
nested = false;
}
callNext = true;
}
};
function startTime() {
if (!STATS)
return;
return Date.now();
}
function endTime(t, message, indent) {
if (!STATS)
return;
var spaces = indent ? indent * 2 : 0;
var time = String(Date.now() - t);
spaces += Math.max(4 - time.length, 0);
var prefix = "";
for (var i = 0; i < spaces; i++)
prefix += " ";
console.log(prefix + time, message);
}
(function() {
var identifierRegexes = {};
var cacheCompletionRegexes = {};
this.enableFeature = function(name, value) {
disabledFeatures[name] = !value;
};
this.setWarningLevel = function(level) {
this.$warningLevel = level;
};
this.setStaticPrefix = completeUtil.setStaticPrefix;
this.setDebug = function(value) {
DEBUG = value;
};
/**
* Registers a handler by loading its code and adding it the handler array
*/
this.register = function(path, contents, callback) {
var _self = this;
function onRegistered(handler) {
handler.$source = path;
handler.proxy = _self.serverProxy;
handler.sender = _self.sender;
handler.$isInited = false;
handler.getEmitter = function(overridePath) {
return _self.$createEmitter(overridePath || path);
};
_self.completionCache = _self.completionPrediction = null;
_self.handlers.push(handler);
_self.$initHandler(handler, null, true, function() {
// Note: may not return for a while for asynchronous workers,
// don't use this for queueing other tasks
_self.sender.emit("registered", { path: path });
callback && callback();
});
}
if (contents) {
// In the context of this worker, we can't use the standard
// require.js approach of using <script/> tags to load scripts,
// but need to load them from the local domain or from text
// instead. For now, we'll just load external plugins from text;
// the UI thread'll have to provide them in that format.
// Note that this indirect eval call evaluates in the (worker)
// global context.
try {
eval.call(null, contents);
} catch (e) {
console.error("Could not load language handler " + path + ": " + e);
_self.sender.emit("registered", { path: path, err: e });
callback && callback(e);
throw e;
}
}
require([path], function(handler) {
if (!handler) {
_self.sender.emit("registered", { path: path, err: "Could not load" });
callback && callback("Could not load");
throw new Error("Could not load language handler " + path);
}
onRegistered(handler);
}, function(e) {
console.error("Could not load language handler " + path + ": " + e);
_self.sender.emit("registered", { path: path, err: e.message });
callback && callback(e);
});
};
this.$createEmitter = function(path) {
var sender = this.sender;
return {
on: function(event, listener) {
sender.on(path + "/" + event, function(e) {
listener(e.data);
});
},
once: function(event, listener) {
sender.once(path + "/" + event, function(e) {
listener(e.data);
});
},
off: function(event, listener) {
sender.off(path + "/" + event, listener);
},
emit: function(event, data) {
sender.emit(path + "/" + event, data);
}
};
};
this.unregister = function(modulePath, callback) {
this.handlers = this.handlers.filter(function(h) {
return h.$source !== modulePath;
});
if (window.require)
window.require.undef(modulePath, true);
callback && callback();
};
this.asyncForEachHandler = function(options, fn, callback) {
var that = this;
var part = options.part;
var method = options.method;
var ignoreSize = options.ignoreSize;
asyncForEach(
this.handlers,
fn,
function(handler) {
return that.isHandlerMatch(handler, part, method, ignoreSize);
},
callback
);
};
this.isHandlerMatch = function(handler, part, method, ignoreSize) {
if (!handler[method]) {
reportError(new Error("Handler " + handler.$source + " does not have method " + method), {
keys: Object.keys(handler),
protoKeys: handler.__proto__ && Object.keys(handler.__proto__)
});
return false;
}
if (handler[method].base_handler)
return;
switch (handler.handlesEditor()) {
case base_handler.HANDLES_EDITOR:
if (this.immediateWindow)
return;
break;
case base_handler.HANDLES_IMMEDIATE:
if (!this.immediateWindow)
return;
}
if (!handler.handlesLanguage(part ? part.language : this.$language, part))
return;
var docLength = ignoreSize ? null : part
? part.getValue().length
: this.doc.$lines.reduce(function(t, l) { return t + l.length; }, 0);
return ignoreSize || docLength < handler.getMaxFileSizeSupported();
};
this.parse = function(part, callback, allowCached, forceCached) {
var value = (part || this.doc).getValue();
var language = part ? part.language : this.$language;
if (allowCached && this.cachedAsts) {
var cached = this.cachedAsts[part.index];
if (cached && cached.ast && cached.part.language === language)
return callback(cached.ast);
}
if (forceCached)
return callback(null);
var resultAst = null;
this.asyncForEachHandler(
{ part: part, method: "parse" },
function parseNext(handler, next) {
if (handler.parse.length === 2) // legacy signature
return handler.parse(value, handleCallbackError(function onParse(ast) {
if (ast) resultAst = ast;
next();
}));
handler.parse(value, {}, handleCallbackError(function onParse(ast) {
if (ast)
resultAst = ast;
next();
}));
},
function() {
callback(resultAst);
}
);
};
/**
* Finds the current node using the language handler.
* This should always be preferred over the treehugger findNode()
* method.
*
* @param pos.row
* @param pos.column
*/
this.findNode = function(ast, pos, callback) {
if (!ast)
return callback();
// Sanity check for old-style pos objects
assert(!pos.line, "Internal error: providing line/col instead of row/column");
var _self = this;
var part = syntaxDetector.getContextSyntaxPart(_self.doc, pos, _self.$language);
if (!part)
return; // cursor position no longer current
var posInPart = syntaxDetector.posToRegion(part.region, pos);
var result;
this.asyncForEachHandler(
{ part: part, method: "findNode" },
function(handler, next) {
handler.findNode(ast, posInPart, handleCallbackError(function(node) {
if (node)
result = node;
next();
}));
},
function() { callback(result); }
);
};
this.outline = function(event) {
var _self = this;
this.getOutline(function(result, isUnordered) {
_self.sender.emit(
"outline",
{
body: result && (result.body || result.items) || [],
path: _self.$path,
isUnordered: isUnordered
}
);
});
};
this.getOutline = function(callback) {
var _self = this;
var result;
var isUnordered = false;
var applySort = false;
this.parse(null, function(ast) {
_self.asyncForEachHandler({ method: "outline" }, function(handler, next) {
if (handler.outline.length === 3) // legacy signature
return handler.outline(_self.doc, ast, handleCallbackError(processResult));
handler.outline(_self.doc, ast, {}, handleCallbackError(processResult));
function processResult(outline) {
if (!outline)
return next();
if (!result || (!outline.isGeneric && result.isGeneric)) {
// Overwrite generic outline
result = outline;
isUnordered = outline.isUnordered;
return next();
}
if (result && outline.isGeneric && !result.isGeneric) {
// Ignore generic outline
return next();
}
// Merging multiple outlines; need to sort them later
applySort = true;
[].push.apply(result.items, outline.items);
result.isGeneric = outline.isGeneric;
next();
}
}, function() {
if (applySort && result)
result.items = result.items.sort(function(a, b) {
return a.pos.sl - b.pos.sl;
});
callback(result, isUnordered);
});
});
};
this.hierarchy = function(event) {
var data = event.data;
var _self = this;
asyncForEach(this.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, null, "hierarchy")) {
handler.hierarchy(_self.doc, data.pos, handleCallbackError(function(hierarchy) {
if (hierarchy)
return _self.sender.emit("hierarchy", hierarchy);
else
next();
}));
}
else
next();
});
};
this.codeFormat = function() {
var _self = this;
asyncForEach(_self.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, null, "codeFormat", true)) {
handler.codeFormat(_self.doc, function(optionalErr, newSource) {
if (typeof optionalErr === "string")
newSource = optionalErr;
else if (optionalErr)
console.error(optionalErr.stack || optionalErr);
if (newSource)
return _self.sender.emit("code_format", newSource);
else
next();
});
}
else
next();
});
};
this.scheduleEmit = function(messageType, data) {
// todo: sender must set the path
data.path = this.$path;
this.sender.emit(messageType, data);
};
/**
* If the program contains a syntax error, the parser will try its best to still produce
* an AST, although it will contain some problems. To avoid that those problems result in
* invalid warning, let's filter out warnings that appear within a line or too after the
* syntax error.
*/
function filterMarkersAroundError(ast, markers) {
if (!ast || !ast.getAnnotation)
return;
var error = ast.getAnnotation("error");
if (!error)
return;
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (marker.type !== 'error' && marker.pos.sl >= error.line && marker.pos.el <= error.line + 2) {
markers.splice(i, 1);
i--;
}
}
}
this.analyze = function(minimalAnalysis, callback) {
var _self = this;
var parts = syntaxDetector.getCodeParts(this.doc, this.$language);
var markers = [];
var cachedAsts = {};
var t0 = startTime();
asyncForEach(parts, function analyzePart(part, nextPart) {
var partMarkers = [];
_self.part = part;
_self.$lastAnalyzer = "parse()";
_self.parse(part, function analyzeParsed(ast) {
cachedAsts[part.index] = { part: part, ast: ast };
_self.asyncForEachHandler(
{ part: part, method: "analyze" },
function(handler, next) {
handler.language = part.language;
var t = startTime();
_self.$lastAnalyzer = handler.$source + ".analyze()";
if (handler.analyze.length === 3 || /^[^)]+minimalAnalysis/.test(handler.analyze.toString())) {
// Legacy signature
return handler.analyze(part.getValue(), ast, handleCallbackError(doNext), minimalAnalysis);
}
handler.analyze(part.getValue(), ast, { path: _self.$path, minimalAnalysis: minimalAnalysis }, handleCallbackError(doNext));
function doNext(result) {
endTime(t, "Analyze: " + handler.$source.replace("plugins/", ""));
if (result)
partMarkers = partMarkers.concat(result);
next();
}
},
function() {
filterMarkersAroundError(ast, partMarkers);
var region = part.region;
partMarkers.forEach(function(marker) {
if (marker.skipMixed)
return;
var pos = marker.pos;
if (!pos)
return console.error("Invalid marker, no position:", marker);
pos.sl = pos.el = pos.sl + region.sl;
if (pos.sl === region.sl) {
pos.sc += region.sc;
pos.ec += region.sc;
}
});
markers = markers.concat(partMarkers);
nextPart();
}
);
});
}, function() {
endTime(t0, "Analyzed all");
_self.cachedAsts = cachedAsts;
if (!minimalAnalysis) {
_self.scheduleEmit("markers", _self.filterMarkersBasedOnLevel(markers));
}
callback();
});
};
this.filterMarkersBasedOnLevel = function(markers) {
if (disabledFeatures.hints)
return [];
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (marker.level && WARNING_LEVELS[marker.level] < WARNING_LEVELS[this.$warningLevel]) {
markers.splice(i, 1);
i--;
}
}
return markers;
};
this.getPart = function (pos) {
return syntaxDetector.getContextSyntaxPart(this.doc, pos, this.$language);
};
/**
* Request the AST node on the current position
*/
this.inspect = function (event) {
var _self = this;
var pos = { row: event.data.row, column: event.data.column };
var part = this.getPart({ row: event.data.row, column: event.data.col });
if (!part)
return; // cursor position no longer current
var partPos = syntaxDetector.posToRegion(part.region, pos);
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(node) {
_self.getPos(node, function(fullPos) {
if (!fullPos) {
var postfix = completeUtil.retrieveFollowingIdentifier(_self.doc.getLine(pos.row), pos.column);
var prefix = completeUtil.retrievePrecedingIdentifier(_self.doc.getLine(pos.row), pos.column);
fullPos = { sl: partPos.row, sc: partPos.column - prefix.length, el: partPos.row, ec: partPos.column + postfix.length };
}
_self.nodeToString(node, function(result) {
// Begin with a simple string representation
var lastResult = {
pos: fullPos,
value: result
};
var rejected;
// Try and find a better match using getInspectExpression()
asyncForEach(_self.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, part, "getInspectExpression")) {
handler.language = part.language;
handler.getInspectExpression(part, ast, partPos, { node: node, path: _self.$path }, handleCallbackError(function(result) {
if (result) {
result.pos = syntaxDetector.posFromRegion(part.region, result.pos);
lastResult = result || lastResult;
}
else if (!rejected) {
lastResult = {};
rejected = true;
}
next();
}));
}
else {
next();
}
}, function () {
if (!lastResult.pos && !lastResult.value)
return _self.scheduleEmit("inspect", lastResult);
// if we have real pos, just get the value from document
var pos = lastResult.pos;
var text = _self.doc.getTextRange({ start: { column: pos.sc, row: pos.sl }, end: { column: pos.ec, row: pos.el }});
if (text != lastResult.value) {
console.warn("inspect expected ", text, " got ", lastResult.value);
lastResult.value = text;
}
_self.scheduleEmit("inspect", lastResult);
});
});
});
});
}, true);
};
this.nodeToString = function(node, callback) {
if (!node)
return callback();
var _self = this;
this.getPos(node, function(pos) {
if (!pos)
return callback();
var doc = _self.doc;
if (pos.sl === pos.el)
return callback(doc.getLine(pos.sl).substring(pos.sc, pos.ec));
var result = doc.getLine(pos.sl).substr(pos.sc);
for (var i = pos.sl + 1; i < pos.el; i++) {
result += doc.getLine(i);
}
result += doc.getLine(pos.el).substr(0, pos.ec);
callback(result);
});
};
this.getPos = function(node, callback) {
if (!node)
return callback();
var done = false;
var _self = this;
this.handlers.forEach(function (h) {
if (!done && _self.isHandlerMatch(h, null, "getPos", true)) {
h.getPos(node, function(result) {
if (!result)
return;
done = true;
callback(result);
});
}
});
if (!done)
callback();
};
this.getIdentifierRegex = function(pos) {
var part = pos && this.getPart(pos);
return identifierRegexes[part ? part.language : this.$language] || completeUtil.DEFAULT_ID_REGEX;
};
this.getCacheCompletionRegex = function(pos) {
var part = pos && this.getPart(pos);
return cacheCompletionRegexes[part ? part.language : this.$language] || completeUtil.DEFAULT_ID_REGEX;
};
/**
* Process a cursor move.
*/
this.onCursorMove = function(event) {
var _self = this;
var pos = event.data.pos;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var line = this.doc.getLine(pos.row);
if (line != event.data.line) {
// Our intelligence is outdated, tell the client
return this.scheduleEmit("hint", { line: null });
}
var result = {
markers: [],
hint: null,
displayPos: null
};
this.initAllRegexes(part.language);
var posInPart = syntaxDetector.posToRegion(part.region, pos);
this.parse(part, function(ast) {
if (!ast)
return callHandlers(ast, null);
_self.findNode(ast, pos, function(currentNode) {
callHandlers(ast, currentNode);
});
}, true, true);
function callHandlers(ast, currentNode) {
asyncForEach(_self.handlers,
function(handler, next) {
if ((pos != _self.lastCurrentPosUnparsed || pos.force) && _self.isHandlerMatch(handler, part, "onCursorMove")) {
handler.onCursorMove(part, ast, posInPart, { node: currentNode, path: _self.$path }, handleCallbackError(function(response) {
processCursorMoveResponse(response, part, result);
next();
}));
}
else {
next();
}
},
function() {
// Send any results so far
_self.lastCurrentPosUnparsed = pos;
if (result.markers.length) {
_self.scheduleEmit("highlightMarkers", disabledFeatures.instanceHighlight
? []
: result.markers
);
event.data.addedMarkers = result.markers;
}
if (result.hint !== null) {
_self.scheduleEmit("hint", {
pos: result.pos,
displayPos: result.displayPos,
message: result.hint,
line: line
});
}
// Parse, analyze, and get more results
_self.onCursorMoveAnalyzed(event);
}
);
}
};
/**
* Perform tooltips/marker analysis after a cursor moved,
* once the document has been parsed & analyzed.
*/
this.onCursorMoveAnalyzed = function(event) {
var _self = this;
var pos = event.data.pos;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var line = this.doc.getLine(pos.row);
if (line != event.data.line) {
// Our intelligence is outdated, tell the client
return this.scheduleEmit("hint", { line: null });
}
if (this.updateScheduled) {
// Postpone the cursor move until the update propagates
this.postponedCursorMove = event;
if (event.data.now)
this.onUpdate(true);
return;
}
var result = {
markers: event.data.addedMarkers || [],
hint: null,
displayPos: null
};
var posInPart = syntaxDetector.posToRegion(part.region, pos);
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(currentNode) {
if (pos != _self.lastCurrentPos || currentNode !== _self.lastCurrentNode || pos.force) {
callHandlers(ast, currentNode);
}
});
}, true);
function callHandlers(ast, currentNode) {
asyncForEach(_self.handlers, function(handler, next) {
if (_self.updateScheduled) {
// Postpone the cursor move until the update propagates
_self.postponedCursorMove = event;
return;
}
if (_self.isHandlerMatch(handler, part, "tooltip") || _self.isHandlerMatch(handler, part, "highlightOccurrences")) {
// We send this to several handlers that each handle part of the language functionality,
// triggered by the cursor move event
assert(!handler.onCursorMovedNode, "handler implements onCursorMovedNode; no longer exists");
asyncForEach(["tooltip", "highlightOccurrences"], function(method, nextMethod) {
handler[method](part, ast, posInPart, { node: currentNode, path: _self.$path }, function(response) {
result = processCursorMoveResponse(response, part, result);
nextMethod();
});
}, next);
}
else {
next();
}
}, function() {
_self.scheduleEmit("highlightMarkers", disabledFeatures.instanceHighlight
? []
: result.markers
);
_self.lastCurrentNode = currentNode;
_self.lastCurrentPos = pos;
_self.scheduleEmit("hint", {
pos: result.pos,
displayPos: result.displayPos,
message: result.hint,
line: line
});
});
}
};
function processCursorMoveResponse(response, part, result) {
if (!response)
return result;
if (response.markers && (!result.markers.found || !response.isGeneric)) {
if (result.markers.isGeneric)
result.markers = [];
result.markers = result.markers.concat(response.markers.map(function (m) {
var start = syntaxDetector.posFromRegion(part.region, { row: m.pos.sl, column: m.pos.sc });
var end = syntaxDetector.posFromRegion(part.region, { row: m.pos.el, column: m.pos.ec });
m.pos = {
sl: start.row,
sc: start.column,
el: end.row,
ec: end.column
};
return m;
}));
result.markers.found = true;
result.markers.isGeneric = response.isGeneric;
}
if (response.hint) {
if (result.hint)
result.hint += "\n" + response.hint;
else
result.hint = response.hint;
}
if (response.pos)
result.pos = response.pos;
if (response.displayPos)
result.displayPos = response.displayPos;
return result;
}
this.$getDefinitionDeclarations = function (row, col, callback) {
var pos = { row: row, column: col };
var allResults = [];
var _self = this;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var posInPart = syntaxDetector.posToRegion(part.region, pos);
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(currentNode) {
asyncForEach(_self.handlers, function jumptodefNext(handler, next) {
if (_self.isHandlerMatch(handler, part, "jumpToDefinition")) {
handler.jumpToDefinition(part, ast, posInPart, { node: currentNode, path: _self.$path, language: _self.$language }, handleCallbackError(function(results) {
handler.path = _self.$path;
if (results)
allResults = allResults.concat(results);
next();
}));
}
else {
next();
}
}, function () {
callback(allResults.map(function (pos) {
var globalPos = syntaxDetector.posFromRegion(part.region, pos);
pos.row = globalPos.row;
pos.column = globalPos.column;
return pos;
}));
});
});
}, true);
};
this.jumpToDefinition = function(event) {
var _self = this;
var pos = event.data;
var line = this.doc.getLine(pos.row);
var regex = this.getIdentifierRegex(pos);
var identifier = completeUtil.retrievePrecedingIdentifier(line, pos.column, regex)
+ completeUtil.retrieveFollowingIdentifier(line, pos.column, regex);
_self.$getDefinitionDeclarations(pos.row, pos.column, function(results) {
_self.sender.emit(
"definition",
{
pos: pos,
results: results || [],
path: _self.$path,
identifier: identifier
}
);
});
};
this.quickfix = function(event) {
var _self = this;
var pos = event.data;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var partPos = syntaxDetector.posToRegion(part.region, pos);
var allResults = [];
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(currentNode) {
asyncForEach(_self.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, part, "getQuickfixes")) {
handler.getQuickfixes(part, ast, partPos, { node: currentNode, path: _self.$path }, handleCallbackError(function(results) {
if (results)
allResults = allResults.concat(results);
next();
}));
}
else {
next();
}
}, function() {
_self.sender.emit("quickfixes_result", {
path: _self.$path,
results: allResults
});
});
});
});
};
this.isJumpToDefinitionAvailable = function(event) {
var _self = this;
var pos = event.data;
_self.$getDefinitionDeclarations(pos.row, pos.column, function(results) {
_self.sender.emit(
"isJumpToDefinitionAvailableResult",
{ value: !!(results && results.length), path: _self.$path, pos: pos }
);
});
};
this.getRefactorings = function(event) {
var _self = this;
var pos = event.data;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var partPos = syntaxDetector.posToRegion(part.region, pos);
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(currentNode) {
var result;
asyncForEach(_self.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, part, "getRefactorings")) {
handler.getRefactorings(part, ast, partPos, { node: currentNode, path: _self.$path }, handleCallbackError(function(response) {
if (response) {
assert(!response.enableRefactorings, "Use refactorings instead of enableRefactorings");
if (!result || result.isGeneric)
result = response;
}
next();
}));
}
else {
next();
}
}, function() {
_self.sender.emit("refactoringsResult", result && result.refactorings || []);
});
});
});
};
this.getRenamePositions = function(event) {
var _self = this;
var pos = event.data;
var part = this.getPart(pos);
if (!part)
return; // cursor position no longer current
var partPos = syntaxDetector.posToRegion(part.region, pos);
function posFromRegion(pos) {
return syntaxDetector.posFromRegion(part.region, pos);
}
this.parse(part, function(ast) {
_self.findNode(ast, pos, function(currentNode) {
var result;
asyncForEach(_self.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, part, "getRenamePositions")) {
assert(!handler.getVariablePositions, "handler implements getVariablePositions, should implement getRenamePositions instead");
handler.getRenamePositions(part, ast, partPos, { node: currentNode, path: _self.$path }, handleCallbackError(function(response) {
if (response) {
if (!result || result.isGeneric)
result = response;
}
next();
}));
}
else {
next();
}
}, function() {
if (!result)
return _self.sender.emit("renamePositionsResult");
result.uses = (result.uses || []).map(posFromRegion);
result.declarations = (result.declarations || []).map(posFromRegion);
result.others = (result.others || []).map(posFromRegion);
result.pos = posFromRegion(result.pos);
_self.sender.emit("renamePositionsResult", result);
});
});
}, true);
};
this.onRenameBegin = function(event) {
var _self = this;
this.handlers.forEach(function(handler) {
if (_self.isHandlerMatch(handler, null, "onRenameBegin"))
handler.onRenameBegin(_self.doc, function() {});
});
};
this.commitRename = function(event) {
var _self = this;
var oldId = event.data.oldId;
var newName = event.data.newName;
var isGeneric = event.data.isGeneric;
var commited = false;
if (oldId.value === newName)
return this.sender.emit("commitRenameResult", {});
asyncForEach(this.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, null, "commitRename")) {
handler.commitRename(_self.doc, oldId, newName, isGeneric, handleCallbackError(function(response) {
if (response) {
commited = true;
_self.sender.emit("commitRenameResult", { err: response, oldName: oldId.value, newName: newName });
// only one handler gets to do this; don't call next();
} else {
next();
}
}));
}
else
next();
},
function() {
if (!commited)
_self.sender.emit("commitRenameResult", {});
}
);
};
this.onRenameCancel = function(event) {
var _self = this;
asyncForEach(this.handlers, function(handler, next) {
if (_self.isHandlerMatch(handler, null, "onRenameCancel")) {
handler.onRenameCancel(handleCallbackError(function() {
next();
}));
}
else {
next();
}
});
};
var updateRunning;
var updateWatchDog;
this.onUpdate = function(now) {
var _self = this;
if (updateRunning) {
// Busy. Try again after last job finishes.
this.updateAgain = { now: now || this.updateAgain && this.updateAgain.now };
return;
}
if (this.updateScheduled && !now) {
// Already scheduled
return;
}
// Cleanup
this.updateAgain = null;
clearTimeout(updateWatchDog);
clearTimeout(this.updateScheduled);
this.updateScheduled = null;
updateWatchDog = setTimeout(function() {
if (DEBUG)
return console.error("Warning: worker analysis taking too long or failed to call back (" + _self.$lastAnalyzer + ")");
_self.updateScheduled = updateRunning = null;
console.error("Warning: worker analysis taking too long or failed to call back (" + _self.$lastAnalyzer + "), rescheduling");
}, UPDATE_TIMEOUT_MAX + this.lastUpdateTime);
if (now) {
doUpdate(function() {
// Schedule another analysis without the now
// and minimalAnalysis options. Disregard updateAgain.
_self.onUpdate();
});
return;
}
this.updateScheduled = setTimeout(function() {
_self.updateScheduled = null;
doUpdate(function() {
if (_self.updateAgain)
_self.onUpdate(_self.updateAgain.now);
});
}, UPDATE_TIMEOUT_MIN + Math.min(this.lastUpdateTime, UPDATE_TIMEOUT_MAX));
function doUpdate(done) {
updateRunning = true;
var beginUpdate = new Date().getTime();
_self.asyncForEachHandler(
{ method: "onUpdate" },
function(handler, next) {
var t = startTime();
handler.onUpdate(_self.doc, handleCallbackError(function() {
endTime(t, "Update: " + handler.$source);
next();
}));
},
function() {
_self.analyze(now, function() {
if (_self.postponedCursorMove) {
_self.onCursorMoveAnalyzed(_self.postponedCursorMove);
_self.postponedCursorMove = null;
}
_self.lastUpdateTime = DEBUG ? 0 : new Date().getTime() - beginUpdate;
clearTimeout(updateWatchDog);
updateRunning = false;
done && done();
});
}
);
}
};
this.$documentToString = function(document) {
if (!document)
return "";
if (Array.isArray(document))
return document.join("\n");
if (typeof document == "string")
return document;
// Convert ArrayBuffer
var array = [];
for (var i = 0; i < document.byteLength; i++) {
array.push(document[i]);
}
return array.join("\n");
};
this.switchFile = function(path, immediateWindow, language, document, pos, workspaceDir) {
var _self = this;
var oldPath = this.$path;
var code = this.$documentToString(document);
this.$workspaceDir = workspaceDir === "" ? "/" : workspaceDir;
this.$path = path;
this.$language = language;
this.doc.$language = language;
this.immediateWindow = immediateWindow;
this.lastCurrentNode = null;
this.lastCurrentPos = null;
this.lastCurrentPosUnparsed = null;
this.cachedAsts = null;
this.setValue(code);
this.lastUpdateTime = 0;
asyncForEach(this.handlers, function(handler, next) {
_self.$initHandler(handler, oldPath, false, next);
}, function() {
_self.onUpdate(true);
});
};
this.$initHandler = function(handler, oldPath, onDocumentOpen, callback) {
var _self = this;
handler.path = this.$path;
handler.language = this.$language;
handler.workspaceDir = this.$workspaceDir;
handler.doc = this.doc;
handler.sender = this.sender;
handler.completeUpdate = this.completeUpdate.bind(this);
handler.immediateWindow = this.immediateWindow;
handler.$getIdentifierRegex = this.getIdentifierRegex.bind(this);
this.initRegexes(handler, this.$language);
if (!handler.$isInited) {
handler.$isInited = true;
handler.init(handleCallbackError(function() {
// Note: may not return for a while for asynchronous workers,
// don't use this for queueing other tasks
if (handler.handlesLanguage(_self.$language))
handler.onDocumentOpen(_self.$path, _self.doc, oldPath, function() {});
handler.$isInitCompleted = true;
callback();
}));
}
else if (onDocumentOpen) {
// Note: may not return for a while for asynchronous workers,
// don't use this for queueing other tasks
if (handler.handlesLanguage(_self.$language))
handler.onDocumentOpen(_self.$path, _self.doc, oldPath, function() {});
callback();
}
else {
callback();
}
};
this.initAllRegexes = function(language) {
if (this.$initedRegexes[language])
return;
this.$initedRegexes[language] = true;
var that = this;
this.handlers.forEach(function(h) {
that.initRegexes(h, language);
});
};
this.initRegexes = function(handler, language) {
if (!handler.handlesLanguage(language))
return;
if (handler.getIdentifierRegex()) {
this.sender.emit("setIdentifierRegex", { language: language, identifierRegex: handler.getIdentifierRegex() });
identifierRegexes[language] = handler.getIdentifierRegex();
}
if (handler.getCacheCompletionRegex()) {
var regex = handler.getCacheCompletionRegex();
if (!/\$$/.test(regex.source))
regex = new RegExp(regex.source + "$");
this.sender.emit("setCacheCompletionRegex", { language: language, cacheCompletionRegex: regex });
cacheCompletionRegexes[language] = regex;
}
if (handler.getCompletionRegex())
this.sender.emit("setCompletionRegex", { language: language, completionRegex: handler.getCompletionRegex() });
if (handler.getTooltipRegex())
this.sender.emit("setTooltipRegex", { language: language, tooltipRegex: handler.getTooltipRegex() });
};
this.documentOpen = function(path, immediateWindow, language, document) {
// Note that we don't set this.$language here, since this document
// may not have focus.
this.$openDocuments["_" + path] = path;
var _self = this;
var code = this.$documentToString(document);
var doc = { getValue: function() { return code; } };
asyncForEach(_self.handlers, function(handler, next) {
if (!handler.handlesLanguage(language))
return next();
handler.onDocumentOpen(path, doc, _self.path, next);
});
};
this.documentClose = function(event) {
var path = event.data;
delete this.$openDocuments["_" + path];
this.asyncForEachHandler({ method: "onDocumentClose" }, function(handler, next) {
handler.onDocumentClose(path, next);
}, function() {});
};
// For code completion
function removeDuplicateMatches(matches) {
// First sort
matches.sort(function(a, b) {
if (a.name < b.name)
return -1;
else if (a.name > b.name)
return 1;
else
return 0;
});
for (var i = 0; i < matches.length - 1; i++) {
var a = matches[i];
var b = matches[i + 1];
if (a.name === b.name || (a.id || a.name) === (b.id || b.name)) {
// Duplicate!
if (a.isContextual && !b.isContextual)
matches.splice(i + 1, 1);
else if (!a.isContextual && b.isContextual)
matches.splice(i, 1);
else if (a.isGeneric && !b.isGeneric)
matches.splice(i, 1);
else if (!a.isGeneric && b.isGeneric)
matches.splice(i + 1, 1);
else if (a.priority < b.priority)
matches.splice(i, 1);
else if (a.priority > b.priority)
matches.splice(i + 1, 1);
else if (a.score < b.score)
matches.splice(i, 1);
else if (a.score > b.score)
matches.splice(i + 1, 1);
else
matches.splice(i, 1);
i--;
}
}
}
this.complete = function(event) {
var _self = this;
var options = event.data;
var pos = options.pos;
_self.waitForCompletionSync(options, function doComplete(identifierRegex) {
var cacheCompletionRegex = _self.getCacheCompletionRegex(pos);
var overrideLine = cacheCompletionRegex && _self.tryShortenCompletionPrefix(_self.doc.getLine(pos.row), pos.column, identifierRegex);
var overridePos = overrideLine != null && { row: pos.row, column: pos.column - 1 };
var newCache = _self.tryCachedCompletion(overridePos || pos, overrideLine, identifierRegex, cacheCompletionRegex, options);
if (!newCache || options.predictOnly) {
// Use existing cache
if (options.predictOnly || _self.completionCache.result)
_self.predictNextCompletion(_self.completionCache, pos, identifierRegex, cacheCompletionRegex, options);
return;
}
_self.completionCache = newCache;
_self.getCompleteHandlerResult(overridePos || pos, overrideLine, identifierRegex, options, function(result) {
if (!result) return;
_self.sender.emit("complete", result);
newCache.setResult(result);
_self.predictNextCompletion(newCache, pos, identifierRegex, cacheCompletionRegex, options);
});
});
};
this.tryShortenCompletionPrefix = function(line, offset, identifierRegex) {
for (var i = 0; i < this.handlers.length; i++) {
if (this.handlers[i].$disableZeroLengthCompletion && this.handlers[i].handlesLanguage(this.$language))
return;
}
// Instead of completing for " i", complete for " ", helping caching and reuse of completions
if (identifierRegex.test(line[offset - 1] || "") && !identifierRegex.test(line[offset - 2] || ""))
return line.substr(0, offset - 1) + line.substr(offset);
};
/**
* Invoke parser and completion handlers to get a completion result.
*/
this.getCompleteHandlerResult = function(pos, overrideLine, identifierRegex, options, callback) {
var _self = this;
var matches = [];
var hadError = false;
var originalLine = _self.doc.getLine(pos.row);
var line = overrideLine != null ? overrideLine : originalLine;
var part = syntaxDetector.getContextSyntaxPart(_self.doc, pos, _self.$language);
if (!part)
return callback(); // cursor position not current
var partPos = syntaxDetector.posToRegion(part.region, pos);
var tStart = startTime();
startOverrideLine();
_self.parse(part, function(ast) {
endTime(tStart, "Complete: parser");
_self.findNode(ast, pos, function(currentNode) {
var handlerOptions = {
noDoc: options.noDoc,
node: currentNode,
language: _self.$language,
path: _self.$path,
line: line,
get identifierPrefix() {
return completeUtil.retrievePrecedingIdentifier(line, pos.column, identifierRegex);
},
};
_self.asyncForEachHandler(
{ part: part, method: "complete" },
function(handler, next) {
handler.language = part.language;
handler.workspaceDir = _self.$workspaceDir;
handler.path = _self.$path;
var t = startTime();
var originalLine2 = _self.doc.getLine(pos.row);
startOverrideLine();
handler.complete(part, ast, partPos, handlerOptions, handleCallbackError(function(completions, handledErr) {
endTime(t, "Complete: " + handler.$source.replace("plugins/", ""), 1);
if (completions && completions.length)
matches = matches.concat(completions);
hadError = !!(hadError || handledErr);
next();
}));
endOverrideLine(originalLine2);
},
function() {
removeDuplicateMatches(matches);
// Sort by priority, score
matches.sort(function(a, b) {
if (a.priority < b.priority)
return 1;
else if (a.priority > b.priority)
return -1;
else if (a.score < b.score)
return 1;
else if (a.score > b.score)
return -1;
else if (a.id && a.id === b.id) {
if (a.isFunction)
return -1;
else if (b.isFunction)
return 1;
}
if (a.name < b.name)
return -1;
else if (a.name > b.name)
return 1;
else
return 0;
});
endTime(tStart, "COMPLETED!");
callback({
pos: pos,
matches: matches,
isUpdate: options.isUpdate,
noDoc: options.noDoc,
hadError: hadError,
line: line,
path: _self.$path,
forceBox: options.forceBox,
deleteSuffix: options.deleteSuffix
}
);
});
});
});
endOverrideLine(originalLine);
// HACK: temporarily override doc contents
function startOverrideLine() {
if (overrideLine != null)
_self.doc.$lines[pos.row] = overrideLine;
_self.$overrideLine = overrideLine;
_self.$lastCompleteRow = pos.row;
}
function endOverrideLine(line) {
_self.$overrideLine = null;
_self.doc.$lines[pos.row] = line;
}
};
/**
* Try to use a cached completion.
*
* @return {Object} a caching key if a new cache needs to be prepared,
* or null in case the previous cache could be used (cache hit)
*/
this.tryCachedCompletion = function(pos, overrideLine, identifierRegex, cacheCompletionRegex, options) {
var that = this;
var cacheKey = this.getCompleteCacheKey(pos, overrideLine, identifierRegex, cacheCompletionRegex, options);
if (options.isUpdate) {
// Updating our cache; return previous cache to update it
if (cacheKey.isCompatible(this.completionCache))
return this.completionCache;
if (cacheKey.isCompatible(this.completionPrediction))
return this.completionPrediction;
}
if (cacheKey.isCompatible(this.completionCache) && !isRecompletionRequired(this.completionCache)) {
if (this.completionCache.result)
cacheHit(this.completionCache);
else
this.completionCache.resultCallbacks.push(cacheHit);
return;
}
if (cacheKey.isCompatible(this.completionPrediction) && !isRecompletionRequired(this.completionPrediction)) {
this.completionCache = this.completionPrediction;
if (this.completionCache.result)
cacheHit(this.completionCache);
else
this.completionCache.resultCallbacks.push(cacheHit);
return;
}
return cacheKey;
function cacheHit(cache) {
if (options.predictOnly)
return;
updateLocalCompletions(that.doc, that.$path, pos, cache.result.matches, function sendCached(err, matches) {
if (err) {
console.error(err);
matches = cache.result.matches;
}
that.sender.emit("complete", {
line: overrideLine != null ? overrideLine : that.doc.getLine(pos.row),
forceBox: options.forceBox,
isUpdate: options.isUpdate,
matches: matches,
path: that.$path,
pos: pos,
noDoc: cache.result.noDoc,
deleteSuffix: options.deleteSuffix,
});
});
}
function isRecompletionRequired(cache) {
// Force recomputing completions for identifiers of a certain length,
// like with tern, which shows different completions for longer prefixes
var recomputeLength = -1;
var recomputeAtOffset1 = false;
for (var i = 0; i < that.handlers.length; i++) {
if (that.handlers[i].$recacheCompletionLength && that.handlers[i].handlesLanguage(that.$language))
recomputeLength = that.handlers[i].$recacheCompletionLength;
if (that.handlers[i].$disableZeroLengthCompletion && that.handlers[i].handlesLanguage(that.$language))
recomputeAtOffset1 = true;
}
if (recomputeAtOffset1 && cacheKey.prefix.length >= 1 && cache.prefix.length === 0)
return true;
return cacheKey.prefix.length >= recomputeLength && cache.prefix.length < recomputeLength;
}
};
/**
* Predict the next completion, given the caching key of the last completion.
*/
this.predictNextCompletion = function(cacheKey, pos, identifierRegex, cacheCompletionRegex, options) {
if (options.isUpdate)
return;
var _self = this;
var predictedString;
var showEarly;
var line = _self.doc.getLine(pos.row);
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, identifierRegex);
this.asyncForEachHandler(
{ method: "predictNextCompletion" },
function preparePredictionInput(handler, next) {
var handlerOptions = {
matches: options.predictOnly ? [] : getFilteredMatches(),
path: _self.$path,
language: _self.$language,
line: line,
identifierPrefix: prefix,
};
handler.predictNextCompletion(_self.doc, null, pos, handlerOptions, handleCallbackError(function(result) {
if (result != null) {
predictedString = result.predicted;
showEarly = result.showEarly;
}
next();
}));
},
function computePrediction() {
if (predictedString == null)
return;
var predictedLine = line.substr(0, pos.column - prefix.length)
+ predictedString
+ line.substr(pos.column);
var predictedPos = { row: pos.row, column: pos.column - prefix.length + predictedString.length };
var predictionKey = _self.getCompleteCacheKey(predictedPos, predictedLine, identifierRegex, cacheCompletionRegex, options);
if (_self.completionPrediction && _self.completionPrediction.isCompatible(predictionKey))
return;
if (_self.completionCache && _self.completionCache.isCompatible(predictionKey))
return;
_self.completionPrediction = predictionKey;
_self.getCompleteHandlerResult(predictedPos, predictedLine, identifierRegex, options, function(result) {
predictionKey.setResult(result);
if (showEarly && cacheKey.isCompatible(_self.completionCache))
showPredictionsEarly(result);
});
}
);
var filteredMatches;
function getFilteredMatches() {
if (filteredMatches)
return filteredMatches;
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, identifierRegex);
filteredMatches = cacheKey.result.matches.filter(function(m) {
m.replaceText = m.replaceText || m.name;
return m.replaceText.indexOf(prefix) === 0;
});
return filteredMatches;
}
function showPredictionsEarly(prediction) {
var newMatches = prediction.matches.filter(function(m) { return m.isContextual; });
if (!newMatches.length)
return;
[].push.apply(_self.completionCache.result.matches, newMatches.map(function(m) {
m = Object.assign({}, m);
m.replaceText = predictedString + m.replaceText;
m.name = predictedString + m.name;
return m;
}));
_self.sender.emit("complete", _self.completionCache.result);
}
};
/**
* Get a key for caching code completion.
* Takes the current document and what not and omits the current identifier from the input
* (which may change as the user types).
*
* @param pos
* @param overrideLine A line to override the current line with while making the key
* @param identifierRegex
*/
this.getCompleteCacheKey = function(pos, overrideLine, identifierRegex, cacheCompletionRegex, options) {
var worker = this;
var doc = this.doc;
var path = this.$path;
var originalLine = doc.getLine(pos.row);
var line = overrideLine != null ? overrideLine : originalLine;
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, identifierRegex);
var suffix = completeUtil.retrieveFollowingIdentifier(line, pos.column, identifierRegex);
var completeLine = removeCacheCompletionPrefix(
line.substr(0, pos.column - prefix.length) + line.substr(pos.column + suffix.length));
var completeLines = doc.$lines.slice();
completeLines[pos.row] = null;
var completePos = { row: pos.row, column: pos.column - prefix.length };
return {
result: null,
resultCallbacks: [],
line: completeLine,
lines: completeLines,
pos: completePos,
prefix: prefix,
path: path,
noDoc: options.noDoc,
setResult: function(result) {
var cacheKey = this;
this.result = result;
this.resultCallbacks.forEach(function(c) {
c(cacheKey);
});
if (result.hadError && worker.completionCache === this)
worker.completionCache = null;
if (result.hadError && worker.completionPrediction === this)
worker.completionPrediction = null;
},
isCompatible: function(other) {
return other
&& other.path === this.path
&& other.pos.row === this.pos.row
&& other.pos.column === this.pos.column
&& other.line === this.line
&& (!other.noDoc || this.noDoc)
&& this.prefix.indexOf(other.prefix) === 0 // match if they're like foo and we're fooo
&& other.lines.length === completeLines.length
&& other.lines[this.pos.row - 1] === completeLines[this.pos.row - 1]
&& other.lines[this.pos.row + 1] === completeLines[this.pos.row + 1]
&& other.lines.every(function(l, i) {
return l === completeLines[i];
});
}
};
function removeCacheCompletionPrefix(line) {
if (!cacheCompletionRegex)
return line;
var match = cacheCompletionRegex.exec(line.substr(0, pos.column - prefix.length));
if (!match)
return line;
pos = { row: pos.row, column: pos.column - match[0].length };
return line.substr(0, line.length - match[0].length);
}
};
/**
* Check if the worker-side copy of the document is still up to date.
* If needed, wait a little while for any pending change events
* if needed (these should normally come in just before the complete event)
*/
this.waitForCompletionSync = function(options, runCompletion) {
var _self = this;
var pos = options.pos;
var line = _self.doc.getLine(pos.row);
this.waitForCompletionSyncThread = this.waitForCompletionSyncThread || 0;
var threadId = ++this.waitForCompletionSyncThread;
var identifierRegex = this.getIdentifierRegex(pos);
if (!completeUtil.canCompleteForChangedLine(line, options.line, pos, pos, identifierRegex)) {
setTimeout(function() {
if (threadId !== _self.waitForCompletionSyncThread)
return;
line = _self.doc.getLine(pos.row);
if (!completeUtil.canCompleteForChangedLine(line, options.line, pos, pos, identifierRegex)) {
setTimeout(function() {
if (threadId !== _self.waitForCompletionSyncThread)
return;
line = _self.doc.getLine(pos.row);
if (!completeUtil.canCompleteForChangedLine(line, options.line, pos, pos, identifierRegex)) {
if (!line) { // sanity check
console.log("worker: seeing an empty line in my copy of the document, won't complete");
}
return console.log("worker: dropped completion request as my copy of the document said: " + line); // ugh give up already
}
runCompletion(identifierRegex);
}, 20);
return;
}
runCompletion(identifierRegex);
}, 5);
return;
}
runCompletion(identifierRegex);
};
/**
* Retrigger completion if the popup is still open and new
* information is now available.
*/
this.completeUpdate = function(pos, line) {
assert(line !== undefined);
this.completionCache = null;
if (!isInWebWorker) { // Avoid making the stack too deep in ?noworker=1 mode
var _self = this;
setTimeout(function onCompleteUpdate() {
_self.complete({ data: { pos: pos, line: line, isUpdate: true }});
}, 0);
}
else {
this.complete({ data: { pos: pos, line: line, isUpdate: true, forceBox: true }});
}
};
/**
* HACK: bypass completion caching for local completer, adding local
* completion results for each new letter typed. Collecting all
* local completions for the empty prefix wouldn't scale...
*/
function updateLocalCompletions(doc, path, pos, matches, callback) {
if (matches.some(function(m) {
return m.isContextual;
}))
return callback(null, matches);
localCompleter.complete(doc, null, pos, null, function(err, results1) {
if (err) return callback(err);
openFilesCompleter.complete(doc, null, pos, { path: path }, function(err, results2) {
if (err) console.error(err);
callback(null, matches.filter(function(m) {
return m.$source !== "local" && m.$source !== "open_files";
}).concat(results1, results2));
});
});
}
function reportError(exception, data) {
if (data)
exception.data = data;
setTimeout(function() {
throw exception; // throw bare exception so it gets reported
});
}
function handleCallbackError(callback) {
return function(optionalErr, result) {
if (optionalErr &&
(optionalErr instanceof Error || typeof optionalErr === "string" || optionalErr.stack || optionalErr.code)) {
if (optionalErr.code !== "ESUPERSEDED")
console.error(optionalErr.stack || optionalErr);
return callback(null, optionalErr);
}
// We only support Error and string errors;
// anything else is treated as a result since legacy
// handlers didn't have an error argument.
callback(optionalErr || result);
};
}
}).call(LanguageWorker.prototype);
});