/*!
 * jQuery UI Effects Pulsate @VERSION
 *
 * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Effects/Pulsate
 *
 * Depends:
 *	jquery.effects.core.js
 */
(function( $, undefined ) {

$.effects.effect.pulsate = function( o, done ) {
	var elem = $( this ),
		mode = $.effects.setMode( elem, o.mode || "show" ),
		show = mode === "show",
		hide = mode === "hide",
		showhide = ( show || mode === "hide" ),

		// showing or hiding leaves of the "last" animation
		anims = ( ( o.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ),
		duration = o.duration / anims,
		animateTo = 0,
		queue = elem.queue(),
		queuelen = queue.length,
		i;

	if ( show || !elem.is(":visible")) {
		elem.css( "opacity", 0 ).show();
		animateTo = 1;
	}

	// anims - 1 opacity "toggles"
	for ( i = 1; i < anims; i++ ) {
		elem.animate({
			opacity: animateTo
		}, duration, o.easing );
		animateTo = 1 - animateTo;
	}

	elem.animate({
		opacity: animateTo
	}, duration, o.easing);

	elem.queue(function() {
		if ( hide ) {
			elem.hide();
		}
		done();
	});

	// We just queued up "anims" animations, we need to put them next in the queue
	if ( queuelen > 1 ) {
		queue.splice.apply( queue,
			[ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
	}
	elem.dequeue();
};

})(jQuery);


this.lastTag.end = this.startOffset + token.end + 1;

if (this.lastTag.tag !== token.contents) {

console.error("Mismatched tag: ", this.lastTag.tag, token.contents);

} else if (token.type === "attribname") {

} else if (token.type === "attribvalue" && attributeName !== null) {

# What: Google Chrome Extension
# Why: To allow apps to (work) be served with the right content type header.
# http://codereview.chromium.org/2830017
# Added by: niftylettuce
application/x-chrome-extension  crx

# What: OTF Message Silencer
# Why: To silence the "Resource interpreted as font but transferred with MIME
# type font/otf" message that occurs in Google Chrome
# Added by: niftylettuce
font/opentype  otf

# What: HTC support
# Why: To properly render .htc files such as CSS3PIE
# Added by: niftylettuce
text/x-component  htc

# What: HTML5 application cache manifest
# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps
# per https://developer.mozilla.org/en/offline_resources_in_firefox
# Added by: louisremi
text/cache-manifest  appcache manifest

# What: node binary buffer format
# Why: semi-standard extension w/in the node community
# Added by: tootallnate
application/octet-stream  buffer

# What: The "protected" MP-4 formats used by iTunes.
# Why: Required for streaming music to browsers (?)
# Added by: broofa
application/mp4  m4p
audio/mp4  m4a

# What: Music playlist format (http://en.wikipedia.org/wiki/M3U)
# Why: See https://github.com/bentomas/node-mime/pull/6
# Added by: mjrusso
application/x-mpegURL  m3u8

# What: Video format, Part of RFC1890
# Why: See https://github.com/bentomas/node-mime/pull/6
# Added by: mjrusso
video/MP2T  ts

# What: The FLAC lossless codec format
# Why: Streaming and serving FLAC audio
# Added by: jacobrask
audio/flac  flac

# What: EventSource mime type
# Why: mime type of Server-Sent Events stream
# http://www.w3.org/TR/eventsource/#text-event-stream
# Added by: francois2metz
text/event-stream  event-stream

# What: Mozilla App manifest mime type
# Why: https://developer.mozilla.org/en/Apps/Manifest#Serving_manifests
# Added by: ednapiranha
application/x-web-app-manifest+json   webapp

# What: Matroska Mime Types
# Why: http://en.wikipedia.org/wiki/Matroska
# Added by: aduncan88
video/x-matroska    mkv
audio/x-matroska    mka


/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, CodeMirror */
/*unittests: HTML Instrumentation*/

/**
 * HTMLInstrumentation
 *
 * This module contains functions for "instrumenting" html code. Instrumented code
 * adds a "data-brackets-id" attribute to every tag in the code. The value of this
 * tag is guaranteed to be unique.
 *
 * The primary function is generateInstrumentedHTML(). This does just what it says -
 * it will read the HTML content in the doc and generate instrumented code by injecting
 * "data-brackets-id" attributes.
 *
 * There are also helper functions for returning the tagID associated with a specified
 * position in the document.
 */
define(function (require, exports, module) {
    "use strict";

    var DocumentManager = require("document/DocumentManager"),
        Tokenizer       = require("language/HTMLTokenizer").Tokenizer,
        PriorityQueue   = require("thirdparty/priority_queue").PriorityQueue,
        MurmurHash3     = require("thirdparty/murmurhash3_gc");

    var seed = Math.floor(Math.random() * 65535);

    // Hash of scanned documents. Key is the full path of the doc. Value is an object
    // with two properties: timestamp and dom. Timestamp is the document timestamp,
    // dom is the root node of a simple DOM tree.
    var _cachedValues = {};

    /**
     * @private
     * Returns true if the specified tag is empty. This could be an empty HTML tag like
     * <meta> or <link>, or a closed tag like

     */
    function _isEmptyTag(payload) {
        if (payload.closed || !payload.nodeName) {
            return true;
        }

        if (/(!doctype|area|base|basefont|br|wbr|col|frame|hr|img|input|isindex|link|meta|param|embed)/i
                .test(payload.nodeName)) {
            return true;
        }

        return false;
    }

    /**
     * Remove a document from the cache
     */
    function _removeDocFromCache(evt, document) {
        if (_cachedValues.hasOwnProperty(document.file.fullPath)) {
            delete _cachedValues[document.file.fullPath];
            $(document).off(".htmlInstrumentation");
        }
    }

    var tagID = 1;

    /**
     * Scan a document to prepare for HTMLInstrumentation
     * @param {Document} doc The doc to scan.
     * @return {Object} Root DOM node of the scanned document.
     */
    function scanDocument(doc) {
        if (!_cachedValues.hasOwnProperty(doc.file.fullPath)) {
            $(doc).on("change.htmlInstrumentation", function () {
                if (_cachedValues[doc.file.fullPath]) {
                    _cachedValues[doc.file.fullPath].dirty = true;
                }
            });

            // Assign to cache, but don't set a value yet
            _cachedValues[doc.file.fullPath] = null;
        }

        var cachedValue = _cachedValues[doc.file.fullPath];
        if (cachedValue && !cachedValue.dirty && cachedValue.timestamp === doc.diskTimestamp) {
            return cachedValue.dom;
        }

        var text = doc.getText(),
            dom = _buildSimpleDOM(text);

        // Cache results
        _cachedValues[doc.file.fullPath] = {
            timestamp: doc.diskTimestamp,
            dom: dom,
            dirty: false
        };

        return dom;
    }

    /**
     * Generate instrumented HTML for the specified document. Each tag has a "data-brackets-id"
     * attribute with a unique ID for its value. For example, "
" becomes something like
     * "
". The attribute value is just a number that is guaranteed
     * to be unique.
     * @param {Document} doc The doc to scan.
     * @return {string} instrumented html content
     */
    function generateInstrumentedHTML(doc) {
        var dom = scanDocument(doc),
            gen = doc.getText();

        // Walk through the dom nodes and insert the 'data-brackets-id' attribute at the
        // end of the open tag
        var insertCount = 0;

        function walk(node) {
            if (node.tag) {
                var attrText = " data-brackets-id='" + node.tagID + "'";

                // Insert the attribute as the first attribute in the tag.
                var insertIndex = node.start + node.tag.length + 1 + insertCount;
                gen = gen.substr(0, insertIndex) + attrText + gen.substr(insertIndex);
                insertCount += attrText.length;
            }

            if (node.children) {
                node.children.forEach(walk);
            }
        }

        walk(dom);
        return gen;
    }

    /**
     * Mark the text for the specified editor. Either the scanDocument() or
     * the generateInstrumentedHTML() function must be called before this function
     * is called.
     *
     * NOTE: This function is "private" for now (has a leading underscore), since
     * the API is likely to change in the future.
     *
     * @param {Editor} editor The editor whose text should be marked.
     * @return none
     */
    function _markText(editor) {
        var cache = _cachedValues[editor.document.file.fullPath],
            dom = cache && cache.dom;

        if (!dom) {
            console.error("Couldn't find the dom for " + editor.document.file.fullPath);
            return;
        }

        _markTextFromDOM(editor, dom);
    }

    function _getMarkerAtDocumentPos(editor, pos) {
        var i,
            cm = editor._codeMirror,
            marks = cm.findMarksAt(pos),
            match;

        var _distance = function (mark) {
            var markerLoc = mark.find();
            if (markerLoc) {
                var markPos = markerLoc.from;
                return (cm.indexFromPos(pos) - cm.indexFromPos(markPos));
            } else {
                return Number.MAX_VALUE;
            }
        };

        for (i = 0; i < marks.length; i++) {
            if (!match) {
                match = marks[i];
            } else {
                if (_distance(marks[i]) < _distance(match)) {
                    match = marks[i];
                }
            }
        }

        return match;
    }

    /**
     * Get the instrumented tagID at the specified position. Returns -1 if
     * there are no instrumented tags at the location.
     * The _markText() function must be called before calling this function.
     *
     * NOTE: This function is "private" for now (has a leading underscore), since
     * the API is likely to change in the future.
     *
     * @param {Editor} editor The editor to scan.
     * @return {number} tagID at the specified position, or -1 if there is no tag
     */
    function _getTagIDAtDocumentPos(editor, pos) {
        var match = _getMarkerAtDocumentPos(editor, pos);

        return (match) ? match.tagID : -1;
    }

    var voidElements = {
        area: true,
        base: true,
        basefont: true,
        br: true,
        col: true,
        command: true,
        embed: true,
        frame: true,
        hr: true,
        img: true,
        input: true,
        isindex: true,
        keygen: true,
        link: true,
        meta: true,
        param: true,
        source: true,
        track: true,
        wbr: true
    };

    function SimpleDOMBuilder(text, startOffset) {
        this.stack = [];
        this.t = new Tokenizer(text, {}, {
            onopentagend: this._handleOpenTagEnd.bind(this),
            onselfclosingtag: function () { },
            oncomment: function () { },
            ontext: function () { }
        });
        this.lastTag = null;
        this.currentTag = null;
        this.startOffset = startOffset || 0;
    }

    function _updateHash(node) {
        if (node.children) {
            var i,
                weight = 0,
                hashes = "";
            for (i = 0; i < node.children.length; i++) {
                weight += node.children[i].weight;
                hashes += node.children[i].signature;
            }
            node.weight = weight;
            node.signature = MurmurHash3.hashString(hashes, hashes.length, seed);
        } else {
            var tagLabel = node.tag + node.tagID;
            node.signature = MurmurHash3.hashString(tagLabel, tagLabel.length, seed);
        }
    }

    SimpleDOMBuilder.prototype._handleOpenTagEnd = function () {
        if (this.currentTag) {
            // We're closing an open tag. Record the end of the open tag as the end of the
            // range. (If we later find a close tag for this tag, the end will get overwritten
            // with the end of the close tag.)
            // TODO: current index is private in the tokenizer right now
            this.currentTag.end = this.startOffset + this.t._index + 1;
            this.lastTag = this.currentTag;
            this.currentTag = null;
        }
    };

    function getTextNodeID(textNode) {
        var childIndex = textNode.parent.children.indexOf(textNode);
        if (childIndex === 0) {
            return textNode.parent.tagID + ".0";
        }
        return textNode.parent.children[childIndex - 1].tagID + "t";
    }

    SimpleDOMBuilder.prototype.build = function () {
        var token, tagLabel;
        var stack = this.stack;
        var attributeName = null;
        var nodeMap = {};
        var signatureMap = {};

        while ((token = this.t.nextToken()) !== null) {
            if (!token.contents) {
                console.error("Token has no contents: ", token);
            } else if (token.type === "opentagname") {
                var newTag = {
                    tag: token.contents,
                    children: [],
                    attributes: {},
                    parent: (stack.length ? stack[stack.length - 1] : null),
                    start: this.startOffset + token.start - 1,
                    weight: 0
                };
                newTag.tagID = this.getID(newTag);
                nodeMap[newTag.tagID] = newTag;
                if (newTag.parent) {
                    newTag.parent.children.push(newTag);
                }
                this.currentTag = newTag;
                if (voidElements.hasOwnProperty(newTag.tag)) {
                    tagLabel = newTag.tag + newTag.tagID;
                    newTag.weight = 0;
                    newTag.signature = MurmurHash3.hashString(tagLabel, tagLabel.length, seed);
                    signatureMap[newTag.signature] = newTag;
                } else {
                    stack.push(newTag);
                }
            } else if (token.type === "closetag") {
                this.lastTag = stack.pop();
                _updateHash(this.lastTag);
                this.lastTag.end = this.startOffset + token.end + 1;
                signatureMap[this.lastTag.signature] = this.lastTag;
                if (this.lastTag.tag !== token.contents) {
                    console.error("Mismatched tag: ", this.lastTag.tag, token.contents);
                }
            } else if (token.type === "attribname") {
                attributeName = token.contents;
            } else if (token.type === "attribvalue" && attributeName !== null) {
                this.currentTag.attributes[attributeName] = token.contents;
                attributeName = null;
            } else if (token.type === "text") {
                // TODO: Not clear why we need this since it should already have been nulled out in
                // _handleOpenTagEnd, but that doesn't seem to be sufficient for some reason.
                this.currentTag = null;
                if (stack.length) {
                    var parent = stack[stack.length - 1];
                    var newNode = {
                        parent: stack[stack.length - 1],
                        content: token.contents,
                        weight: token.contents.length,
                        signature: MurmurHash3.hashString(token.contents, token.contents.length, seed)
                    };
                    parent.children.push(newNode);
                    newNode.tagID = getTextNodeID(newNode);
                    nodeMap[newNode.tagID] = newNode;
                    signatureMap[newNode.signature] = newNode;
                }
            }
        }

        var dom = (stack.length ? stack[0] : this.lastTag);
        dom.nodeMap = nodeMap;
        dom.signatureMap = signatureMap;
        return dom;
    };

    SimpleDOMBuilder.prototype.getID = function () {
        return tagID++;
    };

    function _buildSimpleDOM(text) {
        var builder = new SimpleDOMBuilder(text);
        return builder.build();
    }

    function _dumpDOM(root) {
        var result = "",
            indent = "";

        function walk(node) {
            if (node.tag) {
                result += indent + "TAG " + node.tagID + " " + node.tag + " " + JSON.stringify(node.attributes) + "\n";
            } else {
                result += indent + "TEXT " + node.tagID + " " + node.content + "\n";
            }
            if (node.children) {
                indent += "  ";
                node.children.forEach(walk);
                indent = indent.slice(2);
            }
        }
        walk(root);

        return result;
    }

    // TODO: I'd be worried about stack overflows here
    function _markTags(cm, node) {
        node.children.forEach(function (childNode) {
            if (childNode.tag) {
                _markTags(cm, childNode);
            }
        });
        var mark = cm.markText(cm.posFromIndex(node.start), cm.posFromIndex(node.end));
        mark.tagID = node.tagID;
    }

    function _markTextFromDOM(editor, dom) {
        var cm = editor._codeMirror;

        // Remove existing marks
        var marks = cm.getAllMarks();
        cm.operation(function () {
            marks.forEach(function (mark) {
                if (mark.hasOwnProperty("tagID")) {
                    mark.clear();
                }
            });
        });

        // Mark
        _markTags(cm, dom);
    }

    function DOMUpdater(previousDOM, editor, changeList) {
        var text, startOffset = 0;

        // TODO: if there's more than one change in the changelist, we need to figure out how
        // to find all the affected ranges since we can't directly map the ranges of intermediate
        // changes to marked ranges after the fact.
        //
        // Probably the best we can do is try to determine what the first and last affected positions
        // are in the whole changelist, then figure out what marks those positions are in, and walk
        // up the tree to the common parent. However, that's not trivial since each change's position
        // is specified in terms of the document's state after the previous change. It's probably easy
        // to find the first affected position, but harder to find the last. Perhaps we could just
        // dirty the whole doc after the first affected position.
        //
        // For now, just punt and redo the whole doc if there's more than one change.
        if (changeList && !changeList.next) {
            // If both the beginning and end of the change are still within the same marked
            // range after the change, then assume we can just dirty the tag containing them,
            // otherwise punt. (TODO: We could chase upward in the tree to find the common parent if
            // the tags for the beginning and end of the change are different.)
            var startMark = _getMarkerAtDocumentPos(editor, changeList.from),
                endMark = _getMarkerAtDocumentPos(editor, changeList.to);
            if (startMark && startMark === endMark) {
                var newMarkRange = startMark.find();
                if (newMarkRange) {
                    text = editor._codeMirror.getRange(newMarkRange.from, newMarkRange.to);
                    //console.log("incremental update: " + text);
                    this.changedTagID = startMark.tagID;
                    startOffset = editor._codeMirror.indexFromPos(newMarkRange.from);
                }
            }
        }

        if (!this.changedTagID) {
            // We weren't able to incrementally update, so just rebuild and diff everything.
            text = editor.document.getText();
        }

        this.constructor(text, startOffset);
        this.editor = editor;
        this.cm = editor._codeMirror;
        this.previousDOM = previousDOM;
    }

    DOMUpdater.prototype = new SimpleDOMBuilder();

    DOMUpdater.prototype.getID = function (newTag) {
        // TODO: _getTagIDAtDocumentPos is likely a performance bottleneck
        // Get the mark at the start of the tagname (not before the beginning of the tag, because that's
        // actually inside the parent).
        var currentTagID = _getTagIDAtDocumentPos(this.editor, this.cm.posFromIndex(newTag.start + 1));
        if (currentTagID === -1 || (newTag.parent && currentTagID === newTag.parent.tagID)) {
            currentTagID = tagID++;
        }
        return currentTagID;
    };

    DOMUpdater.prototype._updateMarkedRanges = function (nodeMap) {
        // TODO: this is really inefficient - should we cache the mark for each node?
        var updateIDs = Object.keys(nodeMap),
            cm = this.cm,
            marks = cm.getAllMarks();
        marks.forEach(function (mark) {
            if (mark.hasOwnProperty("tagID") && nodeMap[mark.tagID]) {
                var node = nodeMap[mark.tagID];
                mark.clear();
                mark = cm.markText(cm.posFromIndex(node.start), cm.posFromIndex(node.end));
                mark.tagID = node.tagID;
                updateIDs.splice(updateIDs.indexOf(node.tagID), 1);
            }
        });

        // Any remaining updateIDs are new.
        updateIDs.forEach(function (id) {
            var node = nodeMap[id],
                mark = cm.markText(cm.posFromIndex(node.start), cm.posFromIndex(node.end));
            mark.tagID = id;
        });
    }

    DOMUpdater.prototype._buildNodeMap = function (root) {
        var nodeMap = {};

        function walk(node) {
            if (node.tagID) {
                nodeMap[node.tagID] = node;
            }
            if (node.children) {
                node.children.forEach(walk);
            }
        }

        walk(root);
        root.nodeMap = nodeMap;
    }

    DOMUpdater.prototype._handleDeletions = function (nodeMap, oldSubtreeMap, newSubtreeMap) {
        var deletedIDs = [];
        Object.keys(oldSubtreeMap).forEach(function (key) {
            if (!newSubtreeMap.hasOwnProperty(key)) {
                deletedIDs.push(key);
                delete nodeMap[key];
            }
        });

        // TODO: again, really inefficient - should we cache the mark for each node?
        var marks = this.cm.getAllMarks();
        marks.forEach(function (mark) {
            if (mark.hasOwnProperty("tagID") && deletedIDs.indexOf(mark.tagID) !== -1) {
                mark.clear();
            }
        });
    }

    DOMUpdater.prototype.update = function () {
        var newSubtree = this.build(),
            result = {
                // default result if we didn't identify a changed portion
                newDOM: newSubtree,
                oldSubtree: this.previousDOM,
                newSubtree: newSubtree
            };

        if (this.changedTagID) {
            // Find the old subtree that's going to get swapped out.
            var oldSubtree = this.previousDOM.nodeMap[this.changedTagID],
                parent = oldSubtree.parent;

            // If we didn't have a parent, then the whole tree changed anyway, so
            // we'll just return the default result.
            if (parent) {
                var childIndex = parent.children.indexOf(oldSubtree);
                if (childIndex === -1) {
                    // This should never happen...
                    console.error("DOMUpdater.update(): couldn't locate old subtree in tree");
                } else {
                    // Swap the new subtree in place of the old subtree.
                    oldSubtree.parent = null;
                    newSubtree.parent = parent;
                    parent.children[childIndex] = newSubtree;

                    // Overwrite any node mappings in the parent DOM with the
                    // mappings for the new subtree. We keep the nodeMap around
                    // on the new subtree so that the differ can use it later.
                    // TODO: should we ever null out the nodeMap on the subtree?
                    $.extend(this.previousDOM.nodeMap, newSubtree.nodeMap);

                    // Update marked ranges for all items in the new subtree.
                    this._updateMarkedRanges(newSubtree.nodeMap);

                    // Build a local nodeMap for the old subtree so the differ can
                    // use it.
                    this._buildNodeMap(oldSubtree);

                    // Clean up the info for any deleted nodes that are no longer in
                    // the new tree.
                    this._handleDeletions(this.previousDOM.nodeMap, oldSubtree.nodeMap, newSubtree.nodeMap);

                    // Update the signatures for all parents of the new subtree.
                    var curParent = parent;
                    while (curParent) {
                        _updateHash(curParent);
                        curParent = curParent.parent;
                    }

                    // TODO: add/update marked ranges for any nodes inside subtree
                    // (will need to offset the start/end of each node by the offset
                    // of the beginning of the changed range)

                    result.newDOM = this.previousDOM;
                    result.oldSubtree = oldSubtree;
                }
            }
        }

        return result;
    };

    function attributeCompare(edits, oldNode, newNode) {
        // shallow copy the old attributes object so that we can modify it
        var oldAttributes = $.extend({}, oldNode.attributes),
            newAttributes = newNode.attributes;
        Object.keys(newAttributes).forEach(function (attributeName) {
            if (oldAttributes[attributeName] !== newAttributes[attributeName]) {
                var type = oldAttributes.hasOwnProperty(attributeName) ? "attrChange" : "attrAdd";
                edits.push({
                    type: type,
                    tagID: oldNode.tagID,
                    attribute: attributeName,
                    value: newAttributes[attributeName]
                });
            }
            delete oldAttributes[attributeName];
        });
        Object.keys(oldAttributes).forEach(function (attributeName) {
            edits.push({
                type: "attrDel",
                tagID: oldNode.tagID,
                attribute: attributeName
            });
        });
    }

    function NodeState(node) {
        this.node = node;
        this.childPointer = 0;
    }

    NodeState.prototype.toJSON = function () {
        return {
            tagID: this.node.tagID,
            childPointer: this.childPointer
        };
    };

    function DOMNavigator(root) {
        this.stack = [new NodeState(root)];
    }

    DOMNavigator.prototype.next = function () {
        var parentState = this.stack[this.stack.length - 1];
        var children = parentState.node.children;
        if (parentState.childPointer > children.length - 1) {
            this.stack.pop();
            if (this.stack.length === 0) {
                return null;
            }
            this.stack[this.stack.length - 1].childPointer++;
            return this.next();
        }
        var child = children[parentState.childPointer];
        if (child.children) {
            this.stack.push(new NodeState(child));
            return child;
        } else {
            parentState.childPointer++;
            return child;
        }
    };

    DOMNavigator.prototype.getPosition = function () {
        var parentState = this.stack[this.stack.length - 1];
        return {
            tagID: parentState.node.tagID,
            child: parentState.childPointer - 1
        };
    };

    function domdiffOld(edits, oldNode, newNode) {
        var oldNav = new DOMNavigator(oldNode),
            newNav = new DOMNavigator(newNode);

        attributeCompare(edits, oldNode, newNode);

        var oldChild = oldNav.next(),
            newChild = newNav.next();

        while (oldChild && newChild) {
            if (oldChild.tag && newChild.tag) {
                attributeCompare(edits, oldChild, newChild);

            // Check to see if they're both text nodes
            } else if (!oldChild.tag && !newChild.tag) {
                if (oldChild.content !== newChild.content) {
                    var position = oldNav.getPosition();
                    edits.push({
                        type: "textReplace",
                        tagID: position.tagID,
                        child: position.child,
                        content: newChild.content
                    });
                }
            }
            oldChild = oldNav.next();
            newChild = newNav.next();
        }
    }

    function compareByWeight(a, b) {
        return b.weight - a.weight;
    }

    function domdiff(oldNode, newNode) {
        var queue = new PriorityQueue(compareByWeight),
            edits = [],
            matches = {},
            elementInserts = {},
            textInserts = {},
            textChanges = {},
            currentElement,
            oldElement,
            elementDeletes = {};

        queue.push(newNode);

        while (currentElement = queue.shift()) {
            oldElement = oldNode.nodeMap[currentElement.tagID];
            if (oldElement) {
                matches[currentElement.tagID] = true;
                if (oldElement.children) {
                    if (oldElement.signature !== currentElement.signature) {
                        if (currentElement.children) {
                            currentElement.children.forEach(function (child) {
                                queue.push(child);
                            });
                        }
                    }
                } else {
                    if (oldElement.signature !== currentElement.signature) {
                        textChanges[currentElement.tagID] = true;
                    }
                }
            } else {
                if (currentElement.children) {
                    elementInserts[currentElement.tagID] = true;
                } else {
                    textInserts[currentElement.tagID] = true;
                }
                if (currentElement.children) {
                    currentElement.children.forEach(function (child) {
                        queue.push(child);
                    });
                }
            }
        }

        Object.keys(matches).forEach(function (tagID) {
            var currentElement;
            var subtreeRoot = newNode.nodeMap[tagID];
            if (subtreeRoot.children) {
                attributeCompare(edits, oldNode.nodeMap[tagID], subtreeRoot);
                var nav = new DOMNavigator(subtreeRoot);
                while (currentElement = nav.next()) {
                    var currentTagID = currentElement.tagID;
                    if (!oldNode.nodeMap[currentTagID]) {
                        // this condition can happen for new elements
                        continue;
                    }
                    matches[currentTagID] = true;
                    if (currentElement.children) {
                        attributeCompare(edits, oldNode.nodeMap[currentTagID], currentElement);
                    }
                }
            }
        });

        var initializeTextEdit = function (element) {
            var edit = {
                parentID: element.parent.tagID
            };

            var match = /(\d+)t/.exec(element.tagID);
            if (match) {
                edit.afterID = match[1];
            } else {
                edit.firstChild = true;
            }
            return edit;
        }

        var findDeletions = function (element) {
            if (!matches[element.tagID]) {
                if (element.children) {
                    edits.push({
                        type: "elementDelete",
                        tagID: element.tagID
                    });
                } else {
                    var edit = initializeTextEdit(element);
                    edit.type = "textDelete";
                    edits.push(edit);
                }
            } else if (element.children) {
                var i;
                for (i = 0; i < element.children.length; i++) {
                    findDeletions(element.children[i]);
                }
            }
        };

        findDeletions(oldNode);

        Object.keys(elementInserts).forEach(function (nonMatchingID) {
            var newElement = newNode.nodeMap[nonMatchingID];
            var childIndex = newElement.parent.children.indexOf(newElement);
            edits.push({
                type: "elementInsert",
                tagID: newElement.tagID,
                tag: newElement.tag,
                attributes: newElement.attributes,
                parentID: newElement.parent.tagID,
                child: childIndex
            });
        });

        Object.keys(textInserts).forEach(function (nonMatchingID) {
            var newElement = newNode.nodeMap[nonMatchingID];
            edits.push({
                type: "textInsert",
                tagID: newElement.parent.tagID,
                child: newElement.parent.children.indexOf(newElement),
                content: newElement.content
            });
        });

        Object.keys(textChanges).forEach(function (changedID) {
            var changedElement = newNode.nodeMap[changedID];
            var edit = initializeTextEdit(changedElement);
            edit.type = "textReplace";
            edit.content = changedElement.content;
            edits.push(edit);
        });
        return edits;
    }

    function _updateDOM(previousDOM, editor, changeList) {
        var updater = new DOMUpdater(previousDOM, editor, changeList);
        var result = updater.update();
        var edits = domdiff(result.oldSubtree, result.newSubtree);
        return {
            dom: result.newDOM,
            edits: edits
        };
    }

    function getUnappliedEditList(editor, changeList) {
        var cachedValue = _cachedValues[editor.document.file.fullPath];
        if (!cachedValue || !cachedValue.dom) {
            console.warn("No previous DOM to compare change against");
            return [];
        } else {
            var result = _updateDOM(cachedValue.dom, editor, changeList);
            _cachedValues[editor.document.file.fullPath] = {
                timestamp: editor.document.diskTimestamp, // TODO: update?
                dom: result.dom
            };
            return result.edits;
        }
    }

    $(DocumentManager).on("beforeDocumentDelete", _removeDocFromCache);

    exports.scanDocument = scanDocument;
    exports.generateInstrumentedHTML = generateInstrumentedHTML;
    exports.getUnappliedEditList = getUnappliedEditList;
    exports._markText = _markText;
    exports._getMarkerAtDocumentPos = _getMarkerAtDocumentPos;
    exports._getTagIDAtDocumentPos = _getTagIDAtDocumentPos;
    exports._buildSimpleDOM = _buildSimpleDOM;
    exports._markTextFromDOM = _markTextFromDOM;
    exports._updateDOM = _updateDOM;
    exports._DOMNavigator = DOMNavigator;
    exports._seed = seed;
});


var rimraf = require("../rimraf")
  , path = require("path")
rimraf.sync(path.join(__dirname, "target"))


@charset "utf-8";
#container {
	width:700px;
}
#logo {
	background-image:url(../images/med_logo.png);
	height:100px;
}
nav {
	padding-top:110px;
}
nav ul {
	font-size:18px;
}
nav ul a {
	width:114px;
}
#maincontent {
	padding: 0;
	margin-left:5px;
	width: 700px;
}
#vision {
	background-image:url(../images/med_hero.jpg);
	width: 289px;
	height:217px;
	padding-top:45px;
	padding-right:350px;
	font-size:26px;
}
.pod {
	width: 305px;
}
.podContent {
	width: 302px;
	height:180px;
}
#events {
	margin-left:7px;
}
#news {
	width:650px;
	margin-top:20px;
}
footer p {
	margin-left:5px;
}


/*
 * Metadata - jQuery plugin for parsing metadata from elements
 *
 * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: $Id: jquery.metadata.js 4187 2007-12-16 17:15:27Z joern.zaefferer $
 *
 */

/**
 * Sets the type of metadata to use. Metadata is encoded in JSON, and each property
 * in the JSON will become a property of the element itself.
 *
 * There are three supported types of metadata storage:
 *
 *   attr:  Inside an attribute. The name parameter indicates *which* attribute.
 *
 *   class: Inside the class attribute, wrapped in curly braces: { }
 *
 *   elem:  Inside a child element (e.g. a script tag). The
 *          name parameter indicates *which* element.
 *
 * The metadata for an element is loaded the first time the element is accessed via jQuery.
 *
 * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements
 * matched by expr, then redefine the metadata type and run another $(expr) for other elements.
 *
 * @name $.metadata.setType
 *
 * @example
This is a p

 * @before $.metadata.setType("class")
 * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
 * @desc Reads metadata from the class attribute
 *
 * @example
This is a p

 * @before $.metadata.setType("attr", "data")
 * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
 * @desc Reads metadata from a "data" attribute
 *
 * @example
This is a p

 * @before $.metadata.setType("elem", "script")
 * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
 * @desc Reads metadata from a nested script element
 *
 * @param String type The encoding type
 * @param String name The name of the attribute to be used to get metadata (optional)
 * @cat Plugins/Metadata
 * @descr Sets the type of encoding to be used when loading metadata for the first time
 * @type undefined
 * @see metadata()
 */

(function($) {

$.extend({
	metadata : {
		defaults : {
			type: 'class',
			name: 'metadata',
			cre: /({.*})/,
			single: 'metadata'
		},
		setType: function( type, name ){
			this.defaults.type = type;
			this.defaults.name = name;
		},
		get: function( elem, opts ){
			var settings = $.extend({},this.defaults,opts);
			// check for empty string in single property
			if ( !settings.single.length ) settings.single = 'metadata';

			var data = $.data(elem, settings.single);
			// returned cached data if it already exists
			if ( data ) return data;

			data = "{}";

			if ( settings.type == "class" ) {
				var m = settings.cre.exec( elem.className );
				if ( m )
					data = m[1];
			} else if ( settings.type == "elem" ) {
				if( !elem.getElementsByTagName )
					return undefined;
				var e = elem.getElementsByTagName(settings.name);
				if ( e.length )
					data = $.trim(e[0].innerHTML);
			} else if ( elem.getAttribute != undefined ) {
				var attr = elem.getAttribute( settings.name );
				if ( attr )
					data = attr;
			}

			if ( data.indexOf( '{' ) <0 )
			data = "{" + data + "}";

			data = eval("(" + data + ")");

			$.data( elem, settings.single, data );
			return data;
		}
	}
});

/**
 * Returns the metadata object for the first member of the jQuery object.
 *
 * @name metadata
 * @descr Returns element's metadata object
 * @param Object opts An object contianing settings to override the defaults
 * @type jQuery
 * @cat Plugins/Metadata
 */
$.fn.metadata = function( opts ){
	return $.metadata.get( this[0], opts );
};

})(jQuery);


function _updateDOM(previousDOM, editor, changeList) {

var updater = new DOMUpdater(previousDOM, editor, changeList);

var edits = domdiff(result.oldSubtree, result.newSubtree);


/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, $, brackets, waitsForDone */

function funD_file3(a, b) {
    'use strict';
    var f = function () {return {x: a, y: b}; };
    return f;
}
var kf3 = funD_file3(10, 20);
kf3();

var hope = {frenchçProp2: "test", frenchçProp1: " new value", frenchçfun:frenchçProp() };
function frenchçProp(){
};

function frenchçProp(){
}
frenchçProp();

var 測试 = "test";
測试 = "new value";
// 測 test code hint
function fun測试(){
}
fun測试();  // test function type

function nonAsciiTest() {
    var hope;  // unknown type
    hope["frenchçProp"] = "";
    hope.frenchçProp2 = "test";
    hope.frenchçProp = "frenchçProp";
    fun測试();
    var month = getMonthName(5);
    A1.propA = "test";
}

function frenchçProp(){};
frenchçProp();

function UserException(message) {
   this.message = message;
   this.name = "UserException";
}

function getMonthName(mo) {
   mo = mo-1; // Adjust month number for array index (1=Jan, 12=Dec)
   var months = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
      "Aug", "Sep", "Oct", "Nov", "Dec");
   if (months[mo] !== undefined) {
      return months[mo];
   } else {
      throw new UserException("InvalidMonthNo"); // test exception
   }
}

function callOtherMethods() {
    testTryCatch();

}
function testTryCatch() {
    "use strict";
    try {
        var myMonth = 15; // 15 is out of bound to raise the exception
    } catch (e) {
        myMonth = 12;
        console.log(e.message + e.name); // test e, e.message, e.name
    }
    try {
        var myMonth = 15; // 15 is out of bound to raise the exception
    } catch (e2) {
        myMonth = 12; // test local variable
        console.log(e2.message + e2.name); // test e, e.message, e.name
    }
}


/**
 * This function will be stringified and then injected into every rewired module.
 * Then you can leak private variables by calling myModule.__get__("myPrivateVar");
 *
 * All variables within this function are namespaced in the arguments array because every
 * var declaration could possibly clash with a variable in the module scope.
 *
 * @param {!String} name name of the variable to retrieve
 * @throws {TypeError}
 * @return {*}
 */
function __get__() {
    arguments.varName = arguments[0];
    if (arguments.varName && typeof arguments.varName === "string") {
        return eval(arguments.varName);
    } else {
        throw new TypeError("__get__ expects a non-empty string");
    }
}

module.exports = __get__;


var mkdirp = require('../');
var path = require('path');
var fs = require('fs');
var test = require('tap').test;

test('sync', function (t) {
    t.plan(2);
    var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
    var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
    var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);

    var file = '/tmp/' + [x,y,z].join('/');

    try {
        mkdirp.sync(file, 0755);
    } catch (err) {
        t.fail(err);
        return t.end();
    }

    path.exists(file, function (ex) {
        if (!ex) t.fail('file not created')
        else fs.stat(file, function (err, stat) {
            if (err) t.fail(err)
            else {
                t.equal(stat.mode & 0777, 0755);
                t.ok(stat.isDirectory(), 'target not a directory');
                t.end();
            }
        });
    });
});


var cachedValue = _cachedValues[editor.document.file.fullPath];

console.warn("No previous DOM to compare change against");

var result = _updateDOM(cachedValue.dom, editor, changeList);

// We were in an invalid state, so do a full rebuild.


var common = require('../common');
var assert = common.assert;
var http = require('http');
var path = require('path');
var mime = require('mime');
var request = require('request');
var fs = require('fs');
var FormData = require(common.dir.lib + '/form_data');
var IncomingForm = require('formidable').IncomingForm;

var remoteFile = 'http://nodejs.org/images/logo.png';

// wrap non simple values into function
// just to deal with ReadStream "autostart"
// Can't wait for 0.10
var FIELDS = [
  {name: 'my_field', value: 'my_value'},
  {name: 'my_buffer', value: function(){ return new Buffer([1, 2, 3])} },
  {name: 'my_file', value: function(){ return fs.createReadStream(common.dir.fixture + '/unicycle.jpg')} },
  {name: 'remote_file', value: function(){ return request(remoteFile)} }
];

var server = http.createServer(function(req, res) {

  // formidable is fixed on github
  // but still 7 month old in npm
  //
  // var form = new IncomingForm();
  // form.uploadDir = common.dir.tmp;
  //  form.parse(req);
  // form
  //   .on('field', function(name, value) {
  //     var field = FIELDS.shift();
  //     assert.strictEqual(name, field.name);
  //     assert.strictEqual(value, field.value+'');
  //   })
  //   .on('file', function(name, file) {
  //     var field = FIELDS.shift();
  //     assert.strictEqual(name, field.name);
  //     assert.strictEqual(file.name, path.basename(field.value.path));
  //     // mime.lookup file.NAME == 'my_file' ?
  //     assert.strictEqual(file.type, mime.lookup(file.name));
  //   })
  //   .on('end', function() {
  //     res.writeHead(200);
  //     res.end('done');
  //   });

  // temp workaround
  var data = '';
  req.setEncoding('utf8');
  req.on('data', function(d) {
    data += d;
  });
  req.on('end', function() {
    // check for the fields' traces

    // 1st field : my_field
    var field = FIELDS.shift();
    assert.ok( data.indexOf('form-data; name="'+field.name+'"') != -1 );
    assert.ok( data.indexOf(field.value) != -1 );

    // 2nd field : my_buffer
    var field = FIELDS.shift();
    assert.ok( data.indexOf('form-data; name="'+field.name+'"') != -1 );
    assert.ok( data.indexOf(field.value) != -1 );

    // 3rd field : my_file
    var field = FIELDS.shift();
    assert.ok( data.indexOf('form-data; name="'+field.name+'"') != -1 );
    assert.ok( data.indexOf('; filename="'+path.basename(field.value.path)+'"') != -1 );
    // check for unicycle.jpg traces
    assert.ok( data.indexOf('2005:06:21 01:44:12') != -1 );
    assert.ok( data.indexOf('Content-Type: '+mime.lookup(field.value.path) ) != -1 );

    // 4th field : remote_file
    var field = FIELDS.shift();
    assert.ok( data.indexOf('form-data; name="'+field.name+'"') != -1 );
    assert.ok( data.indexOf('; filename="'+path.basename(field.value.path)+'"') != -1 );
    // check for http://nodejs.org/images/logo.png traces
    assert.ok( data.indexOf('ImageReady') != -1 );
    assert.ok( data.indexOf('Content-Type: '+mime.lookup(remoteFile) ) != -1 );

    res.writeHead(200);
    res.end('done');

  });

});

server.listen(common.port, function() {

  var form = new FormData();

  FIELDS.forEach(function(field) {
    // important to append ReadStreams within the same tick
    if ((typeof field.value == 'function')) {
      field.value = field.value();
    }
    form.append(field.name, field.value);
  });

  form.submit('http://localhost:' + common.port + '/', function(err, res) {

    if (err) {
      throw err;
    }

    assert.strictEqual(res.statusCode, 200);
    server.close();
  });

});

process.on('exit', function() {
  assert.strictEqual(FIELDS.length, 0);
});


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, window, setTimeout */
/*unittests: QuickOpen*/

/*
 * Displays an auto suggest pop-up list of files to allow the user to quickly navigate to a file and lines
 * within a file.
 * Uses FileIndexManger to supply the file list.
 *
 * TODO (issue 333) - currently jquery smart auto complete is used for the pop-up list. While it mostly works
 * it has several issues, so it should be replace with an alternative. Issues:
 * - the pop-up position logic has flaws that require CSS workarounds
 * - the pop-up properties cannot be modified once the object is constructed
 */

define(function (require, exports, module) {
    "use strict";

    var FileIndexManager    = require("project/FileIndexManager"),
        DocumentManager     = require("document/DocumentManager"),
        EditorManager       = require("editor/EditorManager"),
        CommandManager      = require("command/CommandManager"),
        Strings             = require("strings"),
        StringUtils         = require("utils/StringUtils"),
        Commands            = require("command/Commands"),
        ProjectManager      = require("project/ProjectManager"),
        LanguageManager     = require("language/LanguageManager"),
        KeyEvent            = require("utils/KeyEvent"),
        ModalBar            = require("widgets/ModalBar").ModalBar,
        StringMatch         = require("utils/StringMatch"),
        ViewUtils           = require("utils/ViewUtils");

    /** @type Array.<QuickOpenPlugin> */
    var plugins = [];

    /** @type {QuickOpenPlugin} */
    var currentPlugin = null;

    /** @type Array.<FileInfo>*/
    var fileList;

    /** @type $.Promise */
    var fileListPromise;

    /**
     * Remembers the current document that was displayed when showDialog() was called.
     * TODO: in the future, if focusing an item can switch documents, need to restore this on Escape.
     * @type {?string} full path
     */
    var origDocPath;

    /**
     * Remembers the selection in origDocPath that was present when showDialog() was called. Focusing on an
     * item can change the selection; we restore this original selection if the user presses Escape. Null if
     * no document was open when Quick Open was invoked.
     * @type {?{start:{line:number, ch:number}, end:{line:number, ch:number}}}
     */
    var origSelection;

    /**
     * Remembers the scroll position in origDocPath when showDialog() was called (see origSelection above).
     * @type {?{x:number, y:number}}
     */
    var origScrollPos;

    /** @type {boolean} */
    var dialogOpen = false;

    /**
     * The currently open quick open dialog.
     */
    var _curDialog;

    /**
     * Defines API for new QuickOpen plug-ins
     */
    function QuickOpenPlugin(name, languageIds, done, search, match, itemFocus, itemSelect, resultsFormatter, matcherOptions) {
        this.name = name;
        this.languageIds = languageIds;
        this.done = done;
        this.search = search;
        this.match = match;
        this.itemFocus = itemFocus;
        this.itemSelect = itemSelect;
        this.resultsFormatter = resultsFormatter;
        this.matcherOptions = matcherOptions;
    }

    /**
     * Creates and registers a new QuickOpenPlugin
     *
     * @param { name: string,
     *          languageIds:Array.<string>,
     *          done: function(),
     *          search: function(string, !StringMatch.StringMatcher):Array.<SearchResult|string>,
     *          match: function(string):boolean,
     *          itemFocus: function(?SearchResult|string),
     *          itemSelect: funciton(?SearchResult|string),
     *          resultsFormatter: ?function(SearchResult|string, string):string
     *          matcherOptions: Object
     *        } pluginDef
     *
     * Parameter Documentation:
     *
     * name - plug-in name, **must be unique**
     * languageIds - language Ids array. Example: ["javascript", "css", "html"]. An empty array
     *      indicates all language IDs.
     * done - called when quick open is complete. Plug-in should clear its internal state.
     * search - takes a query string and a StringMatcher (the use of which is optional but can speed up your searches) and returns an array of strings that match the query.
     * match - takes a query string and returns true if this plug-in wants to provide
     *      results for this query.
     * itemFocus - performs an action when a result has been highlighted (via arrow keys, mouseover, etc.).
     *      The highlighted search result item (as returned by search()) is passed as an argument.
     * itemSelect - performs an action when a result is chosen.
     *      The selected search result item (as returned by search()) is passed as an argument.
     * resultFormatter - takes a query string and an item string and returns
     *      a <LI> item to insert into the displayed search results. If null, default is provided.
     * matcherOptions - options to pass along to the StringMatcher (see StringMatch.StringMatcher
     *          for available options)
     *
     * If itemFocus() makes changes to the current document or cursor/scroll position and then the user
     * cancels Quick Open (via Esc), those changes are automatically reverted.
     */
    function addQuickOpenPlugin(pluginDef) {
        // Backwards compatibility (for now) for old fileTypes field, if newer languageIds not specified
        if (pluginDef.fileTypes && !pluginDef.languageIds) {
            console.warn("Using fileTypes for QuickOpen plugins is deprecated. Use languageIds instead.");
            pluginDef.languageIds = pluginDef.fileTypes.map(function (extension) {
                return LanguageManager.getLanguageForPath("file." + extension).getId();
            });
            delete pluginDef.fileTypes;
        }

        plugins.push(new QuickOpenPlugin(
            pluginDef.name,
            pluginDef.languageIds,
            pluginDef.done,
            pluginDef.search,
            pluginDef.match,
            pluginDef.itemFocus,
            pluginDef.itemSelect,
            pluginDef.resultsFormatter,
            pluginDef.matcherOptions
        ));
    }

    /**
     * QuickNavigateDialog class
     * @constructor
     */
    function QuickNavigateDialog() {
        this.$searchField = undefined; // defined when showDialog() is called

        // Bind event handlers
        this._handleItemSelect         = this._handleItemSelect.bind(this);
        this._handleItemFocus          = this._handleItemFocus.bind(this);
        this._handleKeyUp              = this._handleKeyUp.bind(this);
        this._handleResultsReady       = this._handleResultsReady.bind(this);
        this._handleShowResults        = this._handleShowResults.bind(this);
        this._handleBlur               = this._handleBlur.bind(this);
        this._handleDocumentMouseDown  = this._handleDocumentMouseDown.bind(this);

        // Bind callbacks from smart-autocomplete
        this._filterCallback           = this._filterCallback.bind(this);
        this._resultsFormatterCallback = this._resultsFormatterCallback.bind(this);

        // StringMatchers that cache in-progress query data.
        this._filenameMatcher           = new StringMatch.StringMatcher({
            segmentedSearch: true
        });
        this._matchers                  = {};
    }

    /**
     * Handles caching of filename search information for the lifetime of a
     * QuickNavigateDialog (a single search until the dialog is dismissed)
     *
     * @type {StringMatch.StringMatcher}
     */
    QuickNavigateDialog.prototype._filenameMatcher = null;

    /**
     * StringMatcher caches for each QuickOpen plugin that keep track of search
     * information for the lifetime of a QuickNavigateDialog (a single search
     * until the dialog is dismissed)
     *
     * @type {Object.<string, StringMatch.StringMatcher>}
     */
    QuickNavigateDialog.prototype._matchers = null;

    function _filenameFromPath(path, includeExtension) {
        var end;
        if (includeExtension) {
            end = path.length;
        } else {
            end = path.lastIndexOf(".");
            if (end === -1) {
                end = path.length;
            }
        }
        return path.slice(path.lastIndexOf("/") + 1, end);
    }

    /**
     * Attempts to extract a line number from the query where the line number
     * is followed by a colon. Callers should explicitly test result with isNaN()
     *
     * @param {string} query string to extract line number from
     * @returns {number} line number. Returns NaN to indicate no line number was found
     */
    function extractLineNumber(query) {
        // only match : at beginning of query for now
        // TODO: match any location of : when QuickOpen._handleItemFocus() is modified to
        // dynamic open files
        if (query.indexOf(":") !== 0) {
            return NaN;
        }

        var result = NaN;
        var regInfo = query.match(/(!?:)(\d+)/); // colon followed by a digit
        if (regInfo) {
            result = regInfo[2] - 1;
        }

        return result;
    }

    /** Returns the last return value of _filterCallback(), which Smart Autocomplete helpfully caches */
    function getLastFilterResult() {
        var cachedResult = $("input#quickOpenSearch").data("smart-autocomplete").rawResults;
        return cachedResult || [];
    }

    /**
     * Converts from list item DOM node to search provider list object
     * @param {jQueryObject} domItem
     * @return {SearchResult|string} value returned from search()
     */
    function domItemToSearchResult(domItem) {
        if (!domItem) {
            return null;
        }

        // Smart Autocomplete uses this assumption internally: index of DOM node in results list container
        // exactly matches index of search result in list returned by _filterCallback()
        var index = $(domItem).index();

        var lastFilterResult = getLastFilterResult();
        return lastFilterResult[index];
    }

    /**
     * Navigates to the appropriate file and file location given the selected item
     * and closes the dialog.
     *
     * Note, if selectedItem is null quick search should inspect $searchField for text
     * that may have not matched anything in in the list, but may have information
     * for carrying out an action (e.g. go to line).
     */
    QuickNavigateDialog.prototype._handleItemSelect = function (e, selectedDOMItem) {

        // This is a work-around to select first item when a selection event occurs
        // (usually from pressing the enter key) and no item is selected in the list.
        // This is a work-around since  Smart auto complete doesn't select the first item
        if (!selectedDOMItem) {
            selectedDOMItem = $(".smart_autocomplete_container > li:first-child").get(0);
        }

        var selectedItem = domItemToSearchResult(selectedDOMItem);

        // Delegate to current plugin
        if (currentPlugin) {
            currentPlugin.itemSelect(selectedItem);
        } else {

            // extract line number, if any
            var query = this.$searchField.val(),
                gotoLine = extractLineNumber(query);

            // Navigate to file and line number
            var fullPath = selectedItem && selectedItem.fullPath;
            if (fullPath) {
                CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, {fullPath: fullPath})
                    .done(function () {
                        if (!isNaN(gotoLine)) {
                            var editor = EditorManager.getCurrentFullEditor();
                            editor.setCursorPos(gotoLine, 0, true);
                        }
                    });
            } else if (!isNaN(gotoLine)) {
                EditorManager.getCurrentFullEditor().setCursorPos(gotoLine, 0, true);
            }
        }

        this._close();
        EditorManager.focusEditor();
    };

    /**
     * Opens the file specified by selected item if there is no current plug-in, otherwise defers handling
     * to the currentPlugin
     */
    QuickNavigateDialog.prototype._handleItemFocus = function (e, selectedDOMItem) {
        var selectedItem = domItemToSearchResult(selectedDOMItem);

        if (currentPlugin) {
            currentPlugin.itemFocus(selectedItem);
        }
        // TODO: Disable opening files on focus for now since this causes focus related bugs between
        // the editor and the search field.
        // Also, see related code in _handleItemFocus
        /*
        else {
            var fullPath = selectedItem.fullPath;
            if (fullPath) {
                CommandManager.execute(Commands.FILE_OPEN, {fullPath: fullPath, focusEditor: false});
            }
        }
        */

    };

    /**
     * Called before Smart Autocomplete processes the key, but after the DOM textfield ($searchField) updates its value.
     * After this, Smart Autocomplete doesn't call _handleFilter() & re-render the list until a setTimeout(0) later.
     */
    QuickNavigateDialog.prototype._handleKeyUp = function (e) {
        // Cancel the search on Esc key, and finish the search on Enter key
        if (e.keyCode === KeyEvent.DOM_VK_RETURN || e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
            // Smart Autocomplete also handles Enter; but it does so without a timeout, which causes #1855.
            // Since our listener was added first (see showDialog()), we can steal the Enter event and block
            // Smart Autocomplete from buggily acting on it.
            e.stopImmediatePropagation();
            e.preventDefault();

            // Process on a timeout since letter keys are handled that way and we don't want to get ahead
            // of processing letters that were typed before the Enter key. The ideal order of events is:
            //   letter keydown/keyup, letter key processed async, enter keydown/keyup, enter key processed async
            // However, we might get 'enter keyup' before 'letter key processed async'. The letter key's
            // timeout will always run before ours since it was registered first.
            var self = this;
            setTimeout(function () {
                if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                    self._close()
                        .done(function () {
                            // Restore original selection / scroll pos
                            if (origSelection) {
                                EditorManager.getCurrentFullEditor().setSelection(origSelection.start, origSelection.end);
                                EditorManager.getCurrentFullEditor().setScrollPos(origScrollPos.x, origScrollPos.y);
                            }
                        });

                } else if (e.keyCode === KeyEvent.DOM_VK_RETURN) {
                    self._handleItemSelect(null, $(".smart_autocomplete_highlight").get(0));  // calls _close() too
                }
            }, 0);

        }
    };

    /**
     * Checks if the given query string is a line number query that is either empty (the number hasn't been typed yet)
     * or is a valid line number within the visible range of the current full editor.
     * @param {string} query The query to check.
     * @return {boolean} true if the given query is a valid line number query.
     */
    QuickNavigateDialog.prototype._isValidLineNumberQuery = function (query) {
        // Empty query returns NaN from extractLineNumber, but we want to treat it as valid for UI purposes.
        if (query === ":") {
            return true;
        }

        var lineNum = extractLineNumber(query),
            editor = EditorManager.getCurrentFullEditor();

        // We could just use 0 and lineCount() here, but in future we might want this logic to work for inline editors as well.
        return (!isNaN(lineNum) && editor && lineNum >= editor.getFirstVisibleLine() && lineNum <= editor.getLastVisibleLine());
    };

    /**
     * Called synchronously after _handleFilter(), but before the cached "last result" is updated and before the DOM
     * list items are re-rendered. Both happen synchronously just after we return. Called even when results is empty.
     */
    QuickNavigateDialog.prototype._handleResultsReady = function (e, results) {
        // Give visual clue when there are no results (unless we're in "Go To Line" mode, where there
        // are never results, or we're in file search mode and waiting for the index to get rebuilt)
        var hasNoResults = (results.length === 0 && (fileList || currentPlugin) && !this._isValidLineNumberQuery(this.$searchField.val()));

        ViewUtils.toggleClass(this.$searchField, "no-results", hasNoResults);
    };

    /**
     * Called synchronously after all other processing is done (_handleFilter(), updating cached "last result" and
     * re-rendering DOM list items). NOT called if the last filter action had 0 results.
     */
    QuickNavigateDialog.prototype._handleShowResults = function (e, results) {
        // Scroll to top result (unless some other item has been highlighted by user)
        if ($(".smart_autocomplete_highlight").length === 0) {
            this._handleItemFocus(null, $(".smart_autocomplete_container > li:first-child").get(0));
        }
    };

    /**
     * Closes the search dialog and notifies all quick open plugins that
     * searching is done.
     * @return {$.Promise} Resolved when the search bar is entirely closed.
     */
    QuickNavigateDialog.prototype._close = function () {
        if (!dialogOpen) {
            return new $.Deferred().reject();
        }
        dialogOpen = false;

        var i;
        for (i = 0; i < plugins.length; i++) {
            var plugin = plugins[i];
            plugin.done();
        }

        // Make sure Smart Autocomplete knows its popup is getting closed (in cases where there's no
        // editor to give focus to below, it won't notice otherwise).
        this.$searchField.trigger("lostFocus");

        // Closing the dialog is a little tricky (see #1384): some Smart Autocomplete code may run later (e.g.
        // (because it's a later handler of the event that just triggered _close()), and that code expects to
        // find metadata that it stuffed onto the DOM node earlier. But $.remove() strips that metadata.
        // So we wait until after this call chain is complete before actually closing the dialog.
        var result = new $.Deferred();
        var self = this;
        setTimeout(function () {
            self.modalBar.close();
            result.resolve();
        }, 0);

        $(".smart_autocomplete_container").remove();

        $(window.document).off("mousedown", this._handleDocumentMouseDown);

        return result.promise();
    };

    /**
     * Returns true if the query string doesn't match the query text field. This can happen when _handleFilter()
     * runs slow (either synchronously or async as in searchFileList()). Several key events queue up before filtering
     * is done, and each sets a timeout. After all the key events are handled, we wind up with a queue of timeouts
     * waiting to run, once per key event. All but the last one reflect a stale value of the text field.
     * @param {string} query
     * @return {boolean}
     */
    function queryIsStale(query) {
        var currentQuery = $("input#quickOpenSearch").val();
        return currentQuery !== query;
    }

    function searchFileList(query, matcher) {
        // FileIndexManager may still be loading asynchronously - if so, can't return a result yet
        if (!fileList) {
            // Smart Autocomplete allows us to return a Promise instead...
            var asyncResult = new $.Deferred();
            fileListPromise.done(function () {
                // ...but it's not very robust. If a previous Promise is obsoleted by the query string changing, it
                // keeps listening to it anyway. So the last Promise to resolve "wins" the UI update even if it's for
                // a stale query. Guard from that by checking that filter text hasn't changed while we were waiting:
                if (!queryIsStale(query)) {
                    // We're still the current query. Synchronously re-run the search call and resolve with its results
                    asyncResult.resolve(searchFileList(query, matcher));
                } else {
                    asyncResult.reject();
                }
            });
            return asyncResult.promise();
        }

        // First pass: filter based on search string; convert to SearchResults containing extra info
        // for sorting & display
        var filteredList = $.map(fileList, function (fileInfo) {
            // Is it a match at all?
            // match query against the full path (with gaps between query characters allowed)
            var searchResult = matcher.match(ProjectManager.makeProjectRelativeIfPossible(fileInfo.fullPath), query);
            if (searchResult) {
                searchResult.label = fileInfo.name;
                searchResult.fullPath = fileInfo.fullPath;
                searchResult.filenameWithoutExtension = _filenameFromPath(fileInfo.name, false);
            }
            return searchResult;
        });

        // Sort by "match goodness" tier first, then within each tier sort alphabetically - first by filename
        // sans extension, (so that "abc.js" comes before "abc-d.js"), then by filename, and finally (for
        // identically-named files) by full path
        StringMatch.multiFieldSort(filteredList, { matchGoodness: 0, filenameWithoutExtension: 1, label: 2, fullPath: 3 });

        return filteredList;
    }

    /**
     * Handles changes to the current query in the search field.
     * @param {string} query The new query.
     * @return {Array} The filtered list of results.
     */
    QuickNavigateDialog.prototype._filterCallback = function (query) {
        // If previous filter calls ran slow, we may have accumulated several query change events in the meantime.
        // Only respond to the one that's current. Note that this only works because we're called on a timeout after
        // the key event; checking DURING the key event itself would never yield a future value for the input field.
        if (queryIsStale(query)) {
            return getLastFilterResult();
        }

        // Reflect current search mode in UI
        this._updateDialogLabel(query);

        // "Go to line" mode is special-cased
        var gotoLine = extractLineNumber(query);
        if (!isNaN(gotoLine)) {
            var from = {line: gotoLine, ch: 0};
            var to = {line: gotoLine, ch: 99999};

            EditorManager.getCurrentFullEditor().setSelection(from, to, true);
        }

        // Try to invoke a search plugin
        var curDoc = DocumentManager.getCurrentDocument();
        if (curDoc) {
            var languageId = curDoc.getLanguage().getId();

            var i;
            for (i = 0; i < plugins.length; i++) {
                var plugin = plugins[i];
                var languageIdMatch = plugin.languageIds.indexOf(languageId) !== -1 || plugin.languageIds.length === 0;
                if (languageIdMatch && plugin.match && plugin.match(query)) {
                    currentPlugin = plugin;

                    // Look up the StringMatcher for this plugin.
                    var matcher = this._matchers[currentPlugin.name];
                    if (!matcher) {
                        matcher = new StringMatch.StringMatcher(plugin.matcherOptions);
                        this._matchers[currentPlugin.name] = matcher;
                    }
                    return plugin.search(query, matcher);
                }
            }
        }

        // No matching plugin: use default file search mode
        currentPlugin = null;
        return searchFileList(query, this._filenameMatcher);
    };

    /**
     * Formats item's label as properly escaped HTML text, highlighting sections that match 'query'.
     * If item is a SearchResult generated by stringMatch(), uses its metadata about which string ranges
     * matched; else formats the label with no highlighting.
     * @param {!string|SearchResult} item
     * @param {?string} matchClass CSS class for highlighting matched text
     * @param {?function(boolean, string):string} rangeFilter
     * @return {!string} bolded, HTML-escaped result
     */
    function highlightMatch(item, matchClass, rangeFilter) {
        var label = item.label || item;
        matchClass = matchClass || "quicksearch-namematch";

        var stringRanges = item.stringRanges;
        if (!stringRanges) {
            // If result didn't come from stringMatch(), highlight nothing
            stringRanges = [{
                text: label,
                matched: false,
                includesLastSegment: true
            }];
        }

        var displayName = "";
        if (item.scoreDebug) {
            var sd = item.scoreDebug;
            displayName += '<span title="sp:' + sd.special + ', m:' + sd.match +
                ', ls:' + sd.lastSegment + ', b:' + sd.beginning +
                ', ld:' + sd.lengthDeduction + ', c:' + sd.consecutive + ', nsos: ' +
                sd.notStartingOnSpecial + '">(' + item.matchGoodness + ') </span>';
        }

        // Put the path pieces together, highlighting the matched parts
        stringRanges.forEach(function (range) {
            if (range.matched) {
                displayName += "<span class='" + matchClass + "'>";
            }

            var rangeText = rangeFilter ? rangeFilter(range.includesLastSegment, range.text) : range.text;
            displayName += StringUtils.breakableUrl(rangeText);

            if (range.matched) {
                displayName += "</span>";
            }
        });
        return displayName;
    }

    function defaultResultsFormatter(item, query) {
        query = query.slice(query.indexOf("@") + 1, query.length);

        var displayName = highlightMatch(item);
        return "<li>" + displayName + "</li>";
    }

    function _filenameResultsFormatter(item, query) {
        // For main label, we just want filename: drop most of the string
        function fileNameFilter(includesLastSegment, rangeText) {
            if (includesLastSegment) {
                var rightmostSlash = rangeText.lastIndexOf('/');
                return rangeText.substring(rightmostSlash + 1);  // safe even if rightmostSlash is -1
            } else {
                return "";
            }
        }
        var displayName = highlightMatch(item, null, fileNameFilter);
        var displayPath = highlightMatch(item, "quicksearch-pathmatch");

        return "<li>" + displayName + "<br /><span class='quick-open-path'>" + displayPath + "</span></li>";
    }

    /**
     * Formats the entry for the given item to be displayed in the dropdown.
     * @param {Object} item The item to be displayed.
     * @return {string} The HTML to be displayed.
     */
    QuickNavigateDialog.prototype._resultsFormatterCallback = function (item) {
        var query = this.$searchField.val();

        var formatter;

        if (currentPlugin) {
            // Plugins use their own formatter or the default formatter
            formatter = currentPlugin.resultsFormatter || defaultResultsFormatter;
        } else {
            // No plugin: default file search mode uses a special formatter
            formatter = _filenameResultsFormatter;
        }
        return formatter(item, query);
    };

    /**
     * Sets the value in the search field, updating the current mode and label based on the
     * given prefix.
     * @param {string} prefix The prefix that determines which mode we're in: must be empty (for file search),
     *      "@" for go to definition, or ":" for go to line.
     * @param {string} initialString The query string to search for (without the prefix).
     */
    QuickNavigateDialog.prototype.setSearchFieldValue = function (prefix, initialString) {
        prefix = prefix || "";
        initialString = initialString || "";
        initialString = prefix + initialString;

        var $field = this.$searchField;
        $field.val(initialString);
        $field.get(0).setSelectionRange(prefix.length, initialString.length);

        // Kick smart-autocomplete to update (it only listens for keyboard events)
        // (due to #1855, this will only pop up results list; it won't auto-"focus" the first result)
        $field.trigger("keyIn", [initialString]);

        this._updateDialogLabel(initialString);
    };

    /**
     * Sets the dialog label based on the type of the given query.
     * @param {string} query The user's current query.
     */
    QuickNavigateDialog.prototype._updateDialogLabel = function (query) {
        var prefix = (query.length > 0 ? query.charAt(0) : "");

        // Update the dialog label based on the current prefix.
        var dialogLabel = "";
        switch (prefix) {
        case ":":
            dialogLabel = Strings.CMD_GOTO_LINE;
            break;
        case "@":
            dialogLabel = Strings.CMD_GOTO_DEFINITION;
            break;
        default:
            dialogLabel = Strings.CMD_QUICK_OPEN;
            break;
        }
        $(".find-dialog-label", this.dialog).text(dialogLabel);
    };

    /**
     * Close the dialog when the user clicks outside of it. Smart-autocomplete listens for this and automatically closes its popup,
     * but we want to close the whole search "dialog." (And we can't just piggyback on the popup closing event, since there are cases
     * where the popup closes that we want the dialog to remain open (e.g. deleting search term via backspace).
     */
    QuickNavigateDialog.prototype._handleDocumentMouseDown = function (e) {
        if (this.modalBar.getRoot().find(e.target).length === 0 && $(".smart_autocomplete_container").find(e.target).length === 0) {
            this._close();
        } else {
            // Allow clicks in the search field to propagate. Clicks in the menu should be
            // blocked to prevent focus from leaving the search field.
            if ($("input#quickOpenSearch").get(0) !== e.target) {
                e.preventDefault();
                e.stopPropagation();
            }
        }
    };

    /**
     * Close the dialog when it loses focus.
     */
    QuickNavigateDialog.prototype._handleBlur = function (e) {
        this._close();
    };

    /**
     * Shows the search dialog and initializes the auto suggestion list with filenames from the current project
     */
    QuickNavigateDialog.prototype.showDialog = function (prefix, initialString) {
        if (dialogOpen) {
            return;
        }
        dialogOpen = true;

        // Global listener to hide search bar & popup
        $(window.document).on("mousedown", this._handleDocumentMouseDown);

        // Record current document & cursor pos so we can restore it if search is canceled
        // We record scroll pos *before* modal bar is opened since we're going to restore it *after* it's closed
        var curDoc = DocumentManager.getCurrentDocument();
        origDocPath = curDoc ? curDoc.file.fullPath : null;
        if (curDoc) {
            origSelection = EditorManager.getCurrentFullEditor().getSelection();
            origScrollPos = EditorManager.getCurrentFullEditor().getScrollPos();
        } else {
            origSelection = null;
            origScrollPos = null;
        }

        // Show the search bar ("dialog")
        var dialogHTML = "<div align='right'><span class='find-dialog-label'></span>: <input type='text' autocomplete='off' id='quickOpenSearch' style='width: 30em'></div>";
        this.modalBar = new ModalBar(dialogHTML, false);
        this.$searchField = $("input#quickOpenSearch");

        // The various listeners registered below fire in this order:
        //   keydown, (async gap), keyup, (async gap), filter, resultsReady, showResults/noResults
        // The later events *always* come after the keydown & keyup (they're triggered on a timeout from keyup). But
        // because of the async gaps, a keydown for the *next* key typed might come *before* they run:
        //   keydown, (async gap), keyup, (async gap), keydown #2, (async gap), filter, resultsReady, showResults/noResults
        // The staleness check in _filterCallback() and the forced async wait in _handleKeyUp() are due to this.

        this.$searchField.bind({
            resultsReady: this._handleResultsReady,
            showResults: this._handleShowResults,
            itemSelect: this._handleItemSelect,
            itemFocus: this._handleItemFocus,
            keyup: this._handleKeyUp,   // it's important we register this BEFORE calling smartAutoComplete(); see handler for details
            blur: this._handleBlur   // can't use lostFocus since smart autocomplete fires it immediately in response to the shortcut's keyup
        });

        this.$searchField.smartAutoComplete({
            source: [],
            maxResults: 20,
            minCharLimit: 0,
            autocompleteFocused: true,
            forceSelect: false,
            typeAhead: false,   // won't work right now because smart auto complete
                                // using internal raw results instead of filtered results for matching
            filter: this._filterCallback,
            resultFormatter: this._resultsFormatterCallback
        });

        this.setSearchFieldValue(prefix, initialString);

        // Start fetching the file list, which will be needed the first time the user enters an un-prefixed query. If FileIndexManager's
        // caches are out of date, this list might take some time to asynchronously build. See searchFileList() for how this is handled.
        fileListPromise = FileIndexManager.getFileInfoList("all")
            .done(function (files) {
                fileList = files;
                fileListPromise = null;
                this._filenameMatcher.reset();
            }.bind(this));
    };

    function getCurrentEditorSelectedText() {
        var currentEditor = EditorManager.getActiveEditor();
        return (currentEditor && currentEditor.getSelectedText()) || "";
    }

    /**
     * Opens the Quick Open bar prepopulated with the given prefix (to select a mode) and optionally
     * with the given query text too. Updates text field contents if Quick Open already open.
     * @param {?string} prefix
     * @param {?string} initialString
     */
    function beginSearch(prefix, initialString) {
        if (dialogOpen) {
            _curDialog.setSearchFieldValue(prefix, initialString);
        } else {
            _curDialog = new QuickNavigateDialog();
            _curDialog.showDialog(prefix, initialString);
        }
    }

    function doFileSearch() {
        beginSearch("", getCurrentEditorSelectedText());
    }

    function doGotoLine() {
        // TODO: Brackets doesn't support disabled menu items right now, when it does goto line and
        // goto definition should be disabled when there is not a current document
        if (DocumentManager.getCurrentDocument()) {
            beginSearch(":", "");
        }
    }

    // TODO: should provide a way for QuickOpenJSSymbol to create this function as a plug-in
    function doDefinitionSearch() {
        if (DocumentManager.getCurrentDocument()) {
            beginSearch("@", getCurrentEditorSelectedText());
        }
    }

    // Listen for a change of project to invalidate our file list
    $(ProjectManager).on("projectOpen", function () {
        fileList = null;
    });

    // TODO: allow QuickOpenJS to register it's own commands and key bindings
    CommandManager.register(Strings.CMD_QUICK_OPEN,         Commands.NAVIGATE_QUICK_OPEN,       doFileSearch);
    CommandManager.register(Strings.CMD_GOTO_DEFINITION,    Commands.NAVIGATE_GOTO_DEFINITION,  doDefinitionSearch);
    CommandManager.register(Strings.CMD_GOTO_LINE,          Commands.NAVIGATE_GOTO_LINE,        doGotoLine);

    exports.beginSearch             = beginSearch;
    exports.addQuickOpenPlugin      = addQuickOpenPlugin;
    exports.highlightMatch          = highlightMatch;

    // accessing these from this module will ultimately be deprecated
    exports.stringMatch             = StringMatch.stringMatch;
    exports.SearchResult            = StringMatch.SearchResult;
    exports.basicMatchSort          = StringMatch.basicMatchSort;
    exports.multiFieldSort          = StringMatch.multiFieldSort;
});


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/* Temporary implementation of unobtrusive scrollbars.
   FUTURE: These currently rely on nonstandard WebKit styles, and don't have all the right behavior.
   Longer-term, we'll need to implement custom scrollbars. */

.quiet-scrollbars ::-webkit-scrollbar {
    width: 9px;
    height: 9px;
}

.quiet-scrollbars ::-webkit-scrollbar-corner {
    background-color: transparent;
}

.quiet-scrollbars ::-webkit-scrollbar-thumb {
    background-color: rgba(0,0,0,0);
}

/* This looks like a dupe, but without it the thumb doesn't fade in
 * properly, probably due to flakiness in how these styles are handled
 * in the build of WebKit that's in CEF.
 */
.quiet-scrollbars ::-webkit-scrollbar-corner {
    background-color: transparent;
}

/* The data URIs for the thumb were generated from the Fireworks files in
 * styles/vertical-thumb-fw-outline.png and styles/horiz-thumb-fw-outline.png.
 */
.quiet-scrollbars #open-files-container:hover::-webkit-scrollbar-thumb:vertical,
.quiet-scrollbars #project-files-container:hover::-webkit-scrollbar-thumb:vertical {
    -webkit-border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAUCAYAAABf2RdVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABV0RVh0Q3JlYXRpb24gVGltZQA0LzIzLzEyckCqugAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAACPSURBVCiR7ZIhDoNAEEXfkJCsxKBJimTPwA3mDHvqsXWrquqaIH5FKUk3XKAJT87789U3SQkYgZ5fNuABvDpgrLUOOefZzBYzW3LOc6112J/pgL6UMkZE+lZERCqlHO0m6WZmCydICuDencmWK/Tfoc3dn63YbxsAkpKkyd1XwD/eV0nT7jBJAO2Ej+kCvAFNxTqyZCNcEQAAAABJRU5ErkJggg==") 9 0;
    border-color: transparent;
    border-width: 9px 0;
    min-height: 20px;
}

.quiet-scrollbars #open-files-container:hover::-webkit-scrollbar-thumb:horizontal,
.quiet-scrollbars #project-files-container:hover::-webkit-scrollbar-thumb:horizontal {
    -webkit-border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABV0RVh0Q3JlYXRpb24gVGltZQA0LzIzLzEyckCqugAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAACLSURBVCiRrdIxCgIxEEbhfxYFOyuxCCLazjGmTu6YKyRV7hFLCzshlcXCQoqxMWCd5B3gqx6pKgAcAJwA7NFfBVB2P+zsnLvGGI89kois3vtijAGp6sU5d+vFWsy85ZyfpKp3IuIRrKWqj2UG9N8CoFprP6MQM28NLCGE1wgqImtK6Q2g0uxtGjitL9E6N1T9Wl8CAAAAAElFTkSuQmCC") 0 9;
    border-color: transparent;
    border-width: 0 9px;
    min-width: 20px;
}


timestamp: editor.document.diskTimestamp, // TODO: update?

keep replace replace replace replace replace keep keep keep keep


jquery-ui

jQuery UI

Abstractions for low-level interaction and animation, advanced effects and high-level, themeable widgets, built on top of the jQuery JavaScript Library, that you can use to build highly interactive web applications.


/*
 * jsTree apple theme 1.0
 * Supported features: dots/no-dots, icons/no-icons, focused, loading
 * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
 */

.jstree-apple > ul { background:url("bg.jpg") left top repeat; }
.jstree-apple li,
.jstree-apple ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
.jstree-apple li { background-position:-90px 0; background-repeat:repeat-y;  }
.jstree-apple li.jstree-last { background:transparent; }
.jstree-apple .jstree-open > ins { background-position:-72px 0; }
.jstree-apple .jstree-closed > ins { background-position:-54px 0; }
.jstree-apple .jstree-leaf > ins { background-position:-36px 0; }

.jstree-apple a { border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; text-shadow:1px 1px 1px white; }
.jstree-apple .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 3px 0 1px; text-shadow:1px 1px 1px silver; }
.jstree-apple .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 3px 0 1px; }
.jstree-apple a .jstree-icon { background-position:-56px -20px; }
.jstree-apple a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }

.jstree-apple.jstree-focused { background:white; }

.jstree-apple .jstree-no-dots li,
.jstree-apple .jstree-no-dots .jstree-leaf > ins { background:transparent; }
.jstree-apple .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
.jstree-apple .jstree-no-dots .jstree-closed > ins { background-position:0 0; }

.jstree-apple .jstree-no-icons a .jstree-icon { display:none; }

.jstree-apple .jstree-search { font-style:italic; }

.jstree-apple .jstree-no-icons .jstree-checkbox { display:inline-block; }
.jstree-apple .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
.jstree-apple .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
.jstree-apple .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
.jstree-apple .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
.jstree-apple .jstree-checked > a > .checkbox:hover { background-position:-38px -37px; }
.jstree-apple .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
.jstree-apple .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }

#vakata-dragged.jstree-apple ins { background:transparent !important; }
#vakata-dragged.jstree-apple .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
#vakata-dragged.jstree-apple .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
#jstree-marker.jstree-apple { background:url("d.png") -41px -57px no-repeat !important; text-indent:-100px; }

.jstree-apple a.jstree-search { color:aqua; }
.jstree-apple .jstree-locked a { color:silver; cursor:default; }

#vakata-contextmenu.jstree-apple-context,
#vakata-contextmenu.jstree-apple-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
#vakata-contextmenu.jstree-apple-context li { }
#vakata-contextmenu.jstree-apple-context a { color:black; }
#vakata-contextmenu.jstree-apple-context a:hover,
#vakata-contextmenu.jstree-apple-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a,
#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
#vakata-contextmenu.jstree-apple-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
#vakata-contextmenu.jstree-apple-context li ul { margin-left:-4px; }

/* TODO: IE6 support - the `>` selectors */