/**
 * 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);

});
