/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define: false, brackets: false, $ */

/*
* Displays an auto suggest popup list of files to allow the user to quickly navigate to a file.
* Uses FileIndexManger to supply the file list and registers Commands.FILE_QUICK_NAVIGATE with Brackets.
*
* TODO (issue 333) - currently jquery smart auto complete is used for the popup list. While it mostly works
* it has several issues, so it should be replace with an alternative. Issues:
* - only accepts an array of strings. A list of objects is preferred to avoid some work arounds to display
*   both the path and filename.
* - the popup position logic has flaws that require css work-arounds
* - the popup properties cannot be modified once the object is constructed
*/

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

    var FileIndexManager    = brackets.getModule("project/FileIndexManager"),
        DocumentManager     = brackets.getModule("document/DocumentManager"),
        CommandManager      = brackets.getModule("command/CommandManager"),
        EditorManager       = brackets.getModule("editor/EditorManager"),
        Commands            = brackets.getModule("command/Commands"),
        ProjectManager      = brackets.getModule("project/ProjectManager");

    /**
    * QuickNavigateDialog class
    * @constructor
    *
    */
    function QuickNavigateDialog() {
        this.closed = false;
        this.result = null; // $.Deferred
    }

    /**
    * Creates a dialog div floating on top of the current code mirror editor
    */
    QuickNavigateDialog.prototype._createDialogDiv = function (template) {
        var wrap = $("#editorHolder")[0];
        this.dialog = wrap.insertBefore(document.createElement("div"), wrap.firstChild);
        this.dialog.className = "CodeMirror-dialog";
        this.dialog.innerHTML = '
' + template + '
';
    };

    function _filenameFromPath(path) {
        return path.slice(path.lastIndexOf("/") + 1, path.length);
    }

    /**
    * Closes the search dialog and resolves the promise that showDialog returned
    */
    QuickNavigateDialog.prototype._close = function (value) {
        if (this.closed) {
            return;
        }

        this.closed = true;

        this.dialog.parentNode.removeChild(this.dialog);

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

        this.result.resolve(value);
    };

    /**
    * Shows the search dialog and initializes the auto suggestion list with filenames from the current project
    * @param {?string} initialString Default text to prepopulate the search field with
    * @returns {$.Promise} a promise that is resolved when the dialgo closes with the string value from the search field
    */
    QuickNavigateDialog.prototype.showDialog = function (initialString) {
        var that = this;
        var fileInfoList;
        this.result = new $.Deferred();

        FileIndexManager.getFileInfoList("all")
            .done(function (filelistResult) {
                fileInfoList = filelistResult;
                var dialogHTML = 'Quick Open: <input type="text" autocomplete="off" id="quickFileOpenSearch" style="width: 30em">';
                that._createDialogDiv(dialogHTML);
                var closed = false;
                var searchField = $('input#quickFileOpenSearch');

                searchField.attr("value", initialString || "");
                searchField.get(0).select();

                // auto suggest list helper function
                function _handleResultsFormatter(path) {
                    var filename = _filenameFromPath(path);
                    var rPath = ProjectManager.makeProjectRelativeIfPossible(path);
                    var boldName = filename.replace(new RegExp($('input#quickFileOpenSearch').val(), "gi"), "$&");
                    return "
" + boldName +
                        "
" + rPath + "
";
                }

                // auto suggest list helper function
                function _handleFilter(term, source) {
                    var filteredList = $.map(source, function (fileInfo) {
                        // match term again filename only (not the path)
                        var path = fileInfo.fullPath;
                        var filename = _filenameFromPath(path);
                        if (filename.toLowerCase().indexOf(term.toLowerCase()) !== -1) {
                            return fileInfo.fullPath;
                        } else {
                            return null;
                        }
                    }).sort(function (a, b) {
                        // sort by filename
                        var filenameA = _filenameFromPath(a).toLowerCase();
                        var filenameB = _filenameFromPath(b).toLowerCase();
                        if (filenameA < filenameB) {
                            return -1;
                        } else if (filenameA > filenameB) {
                            return 1;
                        } else {
                            return 0;
                        }
                    });

                    return filteredList;
                }

                // Create the auto suggest list of filenames
                searchField.smartAutoComplete({
                    source: fileInfoList,
                    maxResults: 10,
                    forceSelect: false,
                    typeAhead: true,
                    filter: _handleFilter,
                    resultFormatter: _handleResultsFormatter
                });

                searchField.bind({
                    // close the dialog when the user selects an item
                    itemSelect: function (ev, selected_item) {
                        var value = decodeURIComponent($(selected_item).attr("data-fullpath"));
                        that._close(value);
                    },

                    keydown: function (e) {
                        var query = searchField.val();

                        // close the dialog when the ENTER (23) or ESC (27) key is pressed
                        if ((e.keyCode === 13 && query.charAt(0) === ":") || e.keyCode === 27) {
                            e.stopPropagation();
                            e.preventDefault();

                            // clear the query on ESC key
                            if (e.keyCode === 27) {
                                query = null;
                            }

                            that._close(query);
                            EditorManager.focusEditor();
                        }
                    }

                });

                searchField.focus();
            });

        return this.result;
    };

    /**
    * Displays a non-modal embedded dialog above the code mirror edit that allows the user to quickly
    * navigate to different files and parts of the current file.
    * The search field lists files in the project. The user can enter ":" followed by a line
    * number to navigate to a specific line in the current file.
    */
    function doFileSearch() {
        // Default to searching for the current selection
        var currentEditor = EditorManager.getFocusedEditor();
        var initialString = currentEditor && currentEditor.getSelectedText();

        var dialog = new QuickNavigateDialog();

        dialog.showDialog(initialString)
            .done(function (query) {
                if (query) {
                    if (query.charAt(0) === ":") {
                        var lineNumber = parseInt(query.slice(1, query.length), 10);
                        if (!isNaN(lineNumber)) {
                            var editor = EditorManager.getCurrentFullEditor();
                            editor.setCursorPos(lineNumber - 1, 0);
                        }
                    } else {
                        CommandManager.execute(Commands.FILE_OPEN, {fullPath: query});
                    }
                }
            });
    }

    CommandManager.register(Commands.FILE_QUICK_NAVIGATE, doFileSearch);
});


.related {
position: absolute;
top: 0;
left: 1px;
height: 100%;
box-shadow: -1px 0 0 0 #fff;
font-size: 12px;
ul {

keep keep keep keep replace keep keep keep keep

/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 */

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

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

    // Load dependent modules
    var HTMLUtils       = require("language/HTMLUtils"),
        SpecRunnerUtils = require("./SpecRunnerUtils.js"),
        Editor          = require("editor/Editor").Editor;

    //Use a clean version of the editor each time
    var myDocument;
    var myEditor;
    beforeEach(function () {
        // init Editor instance (containing a CodeMirror instance)
        $("body").append("
");
        myDocument = SpecRunnerUtils.createMockDocument("");
        myEditor = new Editor(myDocument, true, "", $("#editor").get(0), {});
    });

    afterEach(function () {
        myEditor.destroy();
        myEditor = null;
        $("#editor").remove();
        myDocument = null;
    });

    function setContentAndUpdatePos(pos, linesBefore, hintLineBefore, hintLineAfter, linesAfter) {
        pos.line = linesBefore.length;
        pos.ch = hintLineBefore.length;
        var finalHintLine = (hintLineAfter ? hintLineBefore + hintLineAfter : hintLineBefore);
        var finalLines = linesBefore.concat([finalHintLine]);
        if (linesAfter) {
            finalLines = finalLines.concat(linesAfter);
        }

        var content = finalLines.join("\n");
        myEditor._setText(content);
    }

    describe("HTMLUtils", function () {

        describe("Html Hinting", function () {
            beforeEach(function () {
                // tell CodeMirror this is html content as the mode is
                //used in determining the hints
                myEditor._codeMirror.setOption("mode", "htmlmixed");
            });

            it("should not find attribute hints in an empty editor", function () {
                var pos = {"ch": 0, "line": 0};
                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo());
            });

            it("should find an attribute as a tag is getting typed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '<p class="');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 0, "p", "class"));
            });

            it("should find an attribute as it's added to a tag", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', '', '
'],
                    '
test
',
                    [ '
', '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 0, "p", "id"));
            });

            it("should find an attribute as the value is typed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', '', '
'],
                    '
test
',
                    [ '
', '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 3, "p", "id", "one"));
            });

            it("should not find an attribute as text is added", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
tricky="', '
',
                    [ '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo());
            });

            it("should find the attribute value if present", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '

',
                    [ '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 3, "p", "class", "foo"));
            });

            it("should find the full attribute as an existing value is changed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '

',
                    [ '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 3, "p", "class", "foo bar"));
            });

            it("should find the attribute value even when there is space around the =", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '

',
                    [ '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 3, "p", "class", "foo"));
            });

            it("should find the attribute value when the IP is after the =", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '

',
                    [ '', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_VALUE, 0, "p", "class", "foo"));
            });

            it("should find the tagname as it's typed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '<di');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.TAG_NAME, 2, "di"));
            });

            it("should hint tagname as the open < is typed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
test
<');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.TAG_NAME));
            });

            it("should find the tagname of the current tag if two tags are right next to each other", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
<span');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.TAG_NAME, 4, "span"));
            });

            it("should hint attributes even if there is a lot of space between the tag name and the next attr name", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
<li  ', '  id="foo"');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_NAME, 0, "li"));
            });

            it("should find the tagname as space is typed before the attr name is added", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
<span ');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo(HTMLUtils.ATTR_NAME, 0, "span"));
            });

            it("should not hint anything after the tag is closed", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo());
            });

            it("should not hint anything after a closing tag", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', ''],
                    '
', '
',
                    ['', '']);

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo());
            });

            it("should not hint anything inside a closing tag", function () {
                var pos = {"ch": 0, "line": 0};
                setContentAndUpdatePos(pos,
                    ['', '', '

'],
                    '</ht', 'ml>');

                var tag = HTMLUtils.getTagInfo(myEditor, pos);
                expect(tag).toEqual(HTMLUtils.createTagInfo());
            });
        });
    });
});


/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define: false, $: false,  describe: false, it: false, expect: false, beforeEach: false, afterEach: false, waitsFor: false, waits: false, runs: false */
define(function (require, exports, module) {
    'use strict';

    var NativeFileSystem    = require("file/NativeFileSystem").NativeFileSystem,
        Commands            = require("command/Commands"),
        FileUtils           = require("file/FileUtils"),
        Async               = require("utils/Async"),
        DocumentManager     = require("document/DocumentManager");

    var TEST_PREFERENCES_KEY    = "com.adobe.brackets.test.preferences",
        OPEN_TAG                = "{{",
        CLOSE_TAG               = "}}",
        RE_MARKER               = /[^\\]?\{\{(\d+)[^\\]?\}\}/g,
        testWindow;

    function getTestRoot() {
        // /path/to/brackets/test/SpecRunner.html
        var path = window.location.pathname;
        path = path.substr(0, path.lastIndexOf("/"));
        path = FileUtils.convertToNativePath(path);
        return path;
    }

    function getTestPath(path) {
        return getTestRoot() + path;
    }

    function getBracketsSourceRoot() {
        var path = window.location.pathname;
        path = path.split("/");
        path = path.slice(0, path.length - 2);
        path.push("src");
        return path.join("/");
    }

    /**
     * Returns a Document suitable for use with an Editor in isolation: i.e., a Document that will
     * never be set as the currentDocument or added to the working set.
     */
    function createMockDocument(initialContent) {
        // Use unique filename to avoid collissions in open documents list
        var dummyFile = new NativeFileSystem.FileEntry("_unitTestDummyFile_.js");

        var docToShim = new DocumentManager.Document(dummyFile, new Date(), initialContent);

        // Prevent adding doc to global 'open docs' list; prevents leaks or collisions if a test
        // fails to clean up properly (if test fails, or due to an apparent bug with afterEach())
        docToShim.addRef = function () {};
        docToShim.releaseRef = function () {};

        // Prevent adding doc to working set
        docToShim._handleEditorChange = function (event, editor, changeList) {
            this.isDirty = true;

            // TODO: This needs to be kept in sync with Document._handleEditorChange(). In the
            // future, we should fix things so that we either don't need mock documents or that this
            // is factored so it will just run in both.
            $(this).triggerHandler("change", [this, changeList]);
        };
        docToShim.notifySaved = function () {
            throw new Error("Cannot notifySaved() a unit-test dummy Document");
        };
        return docToShim;
    }

    function createTestWindowAndRun(spec, callback) {
        runs(function () {
            // Position popup windows in the lower right so they're out of the way
            var testWindowWid = 1000,
                testWindowHt  =  700,
                testWindowX   = window.screen.availWidth - testWindowWid,
                testWindowY   = window.screen.availHeight - testWindowHt,
                optionsStr    = "left=" + testWindowX + ",top=" + testWindowY +
                                ",width=" + testWindowWid + ",height=" + testWindowHt;
            testWindow = window.open(getBracketsSourceRoot() + "/index.html", "_blank", optionsStr);

            testWindow.executeCommand = function executeCommand(cmd, args) {
                return testWindow.brackets.test.CommandManager.execute(cmd, args);
            };
        });

        // FIXME (issue #249): Need an event or something a little more reliable...
        waitsFor(
            function () {
                return testWindow.brackets && testWindow.brackets.test;
            },
            5000
        );

        runs(function () {
            // callback allows specs to query the testWindow before they run
            callback.call(spec, testWindow);
        });
    }

    function closeTestWindow() {
        // debug-only to see testWindow state before closing
        // waits(500);

        runs(function () {
            //we need to mark the documents as not dirty before we close
            //or the window will stay open prompting to save
            var openDocs = testWindow.brackets.test.DocumentManager.getAllOpenDocuments();
            openDocs.forEach(function resetDoc(doc) {
                if (doc.isDirty) {
                    //just refresh it back to it's current text. This will mark it
                    //clean to save
                    doc.refreshText(doc.getText(), doc.diskTimestamp);
                }
            });
            testWindow.close();
        });
    }

    /**
     * Dismiss the currently open dialog as if the user had chosen the given button. Dialogs close
     * asynchronously; after calling this, you need to start a new runs() block before testing the
     * outcome.
     * @param {string} buttonId  One of the Dialogs.DIALOG_BTN_* symbolic constants.
     */
    function clickDialogButton(buttonId) {
        runs(function () {
            // Make sure there's one and only one dialog open
            expect(testWindow.$(".modal.instance").length).toBe(1);

            // Make sure desired button exists
            var dismissButton = testWindow.$(".modal.instance .dialog-button[data-button-id='" + buttonId + "']");
            expect(dismissButton.length).toBe(1);

            dismissButton.click();
        });
        // Wait until dialog's result handler runs; it's done on a timeout to avoid Bootstrap bugs
        // TODO: add unit-test helper API to Dialogs that cleanly tell us when it's done closing
        waits(100);
    }

    function loadProjectInTestWindow(path) {
        var isReady = false;

        runs(function () {
            // begin loading project path
            var result = testWindow.brackets.test.ProjectManager.loadProject(path);
            result.done(function () {
                isReady = true;
            });
        });

        // wait for file system to finish loading
        waitsFor(function () { return isReady; }, "loadProject() timeout", 1000);
    }

    /**
     * Parses offsets from text offset markup (e.g. "{{1}}" for offset 1).
     * @param {!string} text Text to parse
     * @return {!{offsets:!Array.<{line:number, ch:number}>, text:!string, original:!string}}
     */
    function parseOffsetsFromText(text) {
        var offsets = [],
            output  = [],
            i       = 0,
            line    = 0,
            char    = 0,
            ch      = 0,
            length  = text.length,
            exec    = null,
            found   = false;

        while (i < length) {
            found = false;

            if (text.slice(i, i + OPEN_TAG.length) === OPEN_TAG) {
                // find "{{[0-9]+}}"
                RE_MARKER.lastIndex = i;
                exec = RE_MARKER.exec(text);
                found = (exec !== null);

                if (found) {
                    // record offset info
                    offsets[exec[1]] = {line: line, ch: ch};

                    // advance
                    i += exec[0].length;
                }
            }

            if (!found) {
                char = text.substr(i, 1);
                output.push(char);

                if (char === '\n') {
                    line++;
                    ch = 0;
                } else {
                    ch++;
                }

                i++;
            }
        }

        return {offsets: offsets, text: output.join(""), original: text};
    }

    /**
     * Creates absolute paths based on the test window's current project
     * @param {!Array.<string>|string} paths Project relative file path(s) to convert. May pass a single string path or array.
     * @return {!Array.<string>|string} Absolute file path(s)
     */
    function makeAbsolute(paths) {
        var fullPath = testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath;

        function prefixProjectPath(path) {
            if (path.indexOf(fullPath) === 0) {
                return path;
            }

            return fullPath + path;
        }

        if (Array.isArray(paths)) {
            return paths.map(prefixProjectPath);
        } else {
            return prefixProjectPath(paths);
        }
    }

    /**
     * Creates relative paths based on the test window's current project. Any paths,
     * outside the project are included in the result, but left as absolute paths.
     * @param {!Array.<string>|string} paths Absolute file path(s) to convert. May pass a single string path or array.
     * @return {!Array.<string>|string} Relative file path(s)
     */
    function makeRelative(paths) {
        var fullPath = testWindow.brackets.test.ProjectManager.getProjectRoot().fullPath,
            fullPathLength = fullPath.length;

        function removeProjectPath(path) {
            if (path.indexOf(fullPath) === 0) {
                return path.substring(fullPathLength);
            }

            return path;
        }

        if (Array.isArray(paths)) {
            return paths.map(removeProjectPath);
        } else {
            return removeProjectPath(paths);
        }
    }

    function makeArray(arg) {
        if (!Array.isArray(arg)) {
            return [arg];
        }

        return arg;
    }

    /**
     * Parses offsets from a file using offset markup (e.g. "{{1}}" for offset 1).
     * @param {!FileEntry} entry File to open
     * @return {$.Promise} A promise resolved with a record that contains parsed offsets,
     *  the file text without offset markup, the original file content, and the corresponding
     *  file entry.
     */
    function parseOffsetsFromFile(entry) {
        var result = new $.Deferred();

        FileUtils.readAsText(entry).done(function (text) {
            var info = parseOffsetsFromText(text);
            info.fileEntry = entry;

            result.resolve(info);
        }).fail(function (err) {
            result.reject(err);
        });

        return result.promise();
    }

    /**
     * Opens project relative file paths in the test window
     * @param {!(Array.<string>|string)} paths Project relative file path(s) to open
     * @return {!$.Promise} A promise resolved with a mapping of project-relative path
     *  keys to a corresponding Document
     */
    function openProjectFiles(paths) {
        var result = new $.Deferred(),
            fullpaths = makeArray(makeAbsolute(paths)),
            keys = makeArray(makeRelative(paths)),
            docs = {},
            FileViewController = testWindow.brackets.test.FileViewController;

        Async.doSequentially(fullpaths, function (path, i) {
            var one = new $.Deferred();

            FileViewController.addToWorkingSetAndSelect(path).done(function (doc) {
                docs[keys[i]] = doc;
                one.resolve();
            }).fail(function () {
                one.reject();
            });

            return one.promise();
        }, false).done(function () {
            result.resolve(docs);
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Opens a file path, parses offset markup then saves the results to the original file.
     * @param {!string} path Project relative file path to open
     * @return {$.Promise} A promise resolved with the offset information results of parseOffsetsFromFile.
     */
    function saveFileWithoutOffsets(path) {
        var result = new $.Deferred(),
            fileEntry = new NativeFileSystem.FileEntry(path);

        parseOffsetsFromFile(fileEntry).done(function (info) {
            // rewrite file without offset markup
            FileUtils.writeText(fileEntry, info.text).done(function () {
                result.resolve(info);
            }).fail(function () {
                result.reject();
            });
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Opens an array of file paths, parses offset markup then saves the results to each original file.
     * @param {!Array.<string>|string} paths Project relative or absolute file paths to open. May pass a single string path or array.
     * @return {!$.Promise} A promised resolved with a map of offset information indexed by project-relative file path.
     */
    function saveFilesWithoutOffsets(paths) {
        var result = new $.Deferred(),
            infos  = {},
            fullpaths = makeArray(makeAbsolute(paths)),
            keys = makeArray(makeRelative(paths));

        var parallel = Async.doSequentially(fullpaths, function (path, i) {
            var one = new $.Deferred();

            saveFileWithoutOffsets(path).done(function (info) {
                infos[keys[i]] = info;
                one.resolve();
            }).fail(function () {
                one.reject();
            });

            return one.promise();
        }, false);

        parallel.done(function () {
            result.resolve(infos);
        }).fail(function () {
            result.reject();
        });

        return result.promise();
    }

    /**
     * Restore file content with offset markup. When using saveFileWithoutOffsets(),
     * remember to call this function during spec teardown (after()).
     * @param {$.Promise} A promise resolved when all files are re-written to their original content.
     */
    function saveFilesWithOffsets(infos) {
        var arr = [];
        $.each(infos, function (index, value) {
            arr.push(value);
        });

        return Async.doInParallel(arr, function (info) {
            return FileUtils.writeText(info.fileEntry, info.original);
        }, false);
    }

    /**
     * Set editor cursor position to the given offset then activate an inline editor.
     * @param {!Editor} editor
     * @param {!{line:number, ch:number}} offset
     * @return {$.Promise} a promise that will be resolved when an inline
     *  editor is created or rejected when no inline editors are available.
     */
    function openInlineEditorAtOffset(editor, offset) {
        editor.setCursorPos(offset.line, offset.ch);

        // TODO (jasonsj): refactor CMD+E as a Command instead of a CodeMirror key binding?
        return testWindow.brackets.test.EditorManager._openInlineWidget(editor);
    }

    exports.TEST_PREFERENCES_KEY    = TEST_PREFERENCES_KEY;

    exports.getTestRoot                 = getTestRoot;
    exports.getTestPath                 = getTestPath;
    exports.getBracketsSourceRoot       = getBracketsSourceRoot;
    exports.createMockDocument          = createMockDocument;
    exports.createTestWindowAndRun      = createTestWindowAndRun;
    exports.closeTestWindow             = closeTestWindow;
    exports.clickDialogButton           = clickDialogButton;
    exports.loadProjectInTestWindow     = loadProjectInTestWindow;
    exports.openProjectFiles            = openProjectFiles;
    exports.openInlineEditorAtOffset    = openInlineEditorAtOffset;
    exports.saveFilesWithOffsets        = saveFilesWithOffsets;
    exports.saveFilesWithoutOffsets     = saveFilesWithoutOffsets;
    exports.saveFileWithoutOffsets      = saveFileWithoutOffsets;
});


$('<span>' + " " + token.type.split('->').join(':').trim() + '</span>').appendTo($hintObj).addClass("jshint-description");

$('<span>' + token.type.split('->').join(':').toString().trim() + '</span>').appendTo($hintObj).addClass("jshint-description");

/*
 * 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, brackets, $ */

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

    var _ = brackets.getModule("thirdparty/lodash");

    var CodeHintManager      = brackets.getModule("editor/CodeHintManager"),
        EditorManager        = brackets.getModule("editor/EditorManager"),
        Commands             = brackets.getModule("command/Commands"),
        CommandManager       = brackets.getModule("command/CommandManager"),
        LanguageManager      = brackets.getModule("language/LanguageManager"),
        AppInit              = brackets.getModule("utils/AppInit"),
        ExtensionUtils       = brackets.getModule("utils/ExtensionUtils"),
        StringMatch          = brackets.getModule("utils/StringMatch"),
        ProjectManager       = brackets.getModule("project/ProjectManager"),
        PreferencesManager   = brackets.getModule("preferences/PreferencesManager"),
        Strings              = brackets.getModule("strings"),
        ParameterHintManager = require("ParameterHintManager"),
        HintUtils            = require("HintUtils"),
        ScopeManager         = require("ScopeManager"),
        Session              = require("Session"),
        Acorn                = require("thirdparty/acorn/acorn");

    var session        = null,  // object that encapsulates the current session state
        cachedCursor   = null,  // last cursor of the current hinting session
        cachedHints    = null,  // sorted hints for the current hinting session
        cachedType     = null,  // describes the lookup type and the object context
        cachedToken    = null,  // the token used in the current hinting session
        matcher        = null,  // string matcher for hints
        jsHintsEnabled = true,  // preference setting to enable/disable the hint session
        noHintsOnDot   = false, // preference setting to prevent hints on dot
        ignoreChange;           // can ignore next "change" event if true;

    // Languages that support inline JavaScript
    var _inlineScriptLanguages = ["html", "php"];

    // Define the detectedExclusions which are files that have been detected to cause Tern to run out of control.
    PreferencesManager.definePreference("jscodehints.detectedExclusions", "array", [], {
        description: Strings.DESCRIPTION_DETECTED_EXCLUSIONS
    });

    // This preference controls when Tern will time out when trying to understand files
    PreferencesManager.definePreference("jscodehints.inferenceTimeout", "number", 30000, {
        description: Strings.DESCRIPTION_INFERENCE_TIMEOUT
    });

    // This preference controls whether to prevent hints from being displayed when dot is typed
    PreferencesManager.definePreference("jscodehints.noHintsOnDot", "boolean", false, {
        description: Strings.DESCRIPTION_NO_HINTS_ON_DOT
    });

    // This preference controls whether to create a session and process all JS files or not.
    PreferencesManager.definePreference("codehint.JSHints", "boolean", true, {
        description: Strings.DESCRIPTION_JS_HINTS
    });

    /**
     * Check whether any of code hints preferences for JS Code Hints is disabled
     * @return {boolean} enabled/disabled
     */
    function _areHintsEnabled() {
        return (PreferencesManager.get("codehint.JSHints") !== false) &&
            (PreferencesManager.get("showCodeHints") !== false);
    }

    PreferencesManager.on("change", "codehint.JSHints", function () {
        jsHintsEnabled = _areHintsEnabled();
    });

    PreferencesManager.on("change", "showCodeHints", function () {
        jsHintsEnabled = _areHintsEnabled();
    });

    PreferencesManager.on("change", "jscodehints.noHintsOnDot", function () {
        noHintsOnDot = !!PreferencesManager.get("jscodehints.noHintsOnDot");
    });

    /**
     * Sets the configuration, generally for testing/debugging use.
     * Configuration keys are merged into the current configuration.
     * The Tern worker is automatically updated to the new config as well.
     *
     * * debug: Set to true if you want verbose logging
     * * noReset: Set to true if you don't want the worker to restart periodically
     *
     * @param {Object} configUpdate keys/values to merge into the config
     */
    function setConfig(configUpdate) {
        var config = setConfig.config;
        Object.keys(configUpdate).forEach(function (key) {
            config[key] = configUpdate[key];
        });

        ScopeManager._setConfig(configUpdate);
    }

    setConfig.config = {};

    /**
     *  Get the value of current session.
     *  Used for unit testing.
     * @return {Session} - the current session.
     */
    function getSession() {
        return session;
    }

    /**
     * Creates a hint response object. Filters the hint list using the query
     * string, formats the hints for display, and returns a hint response
     * object according to the CodeHintManager's API for code hint providers.
     *
     * @param {Array.<Object>} hints - hints to be included in the response
     * @param {string} query - querystring with which to filter the hint list
     * @param {Object} type - the type of query, property vs. identifier
     * @return {Object} - hint response as defined by the CodeHintManager API
     */
    function getHintResponse(hints, query, type) {

        var trimmedQuery,
            formattedHints;

        if (setConfig.config.debug) {
            console.debug("Hints", _.pluck(hints, "label"));
        }

        var _infered = true;

        function getInferHelper(type) {
            return function (element, index, array) {
                if (element === type && _infered) {
                    _infered = true;
                } else {
                    _infered = false;
                }
            };
        }

        function inferArrayTypeClass(typeExpr) {
            var type = "type-array";
            var types = typeExpr.split('[')[1].split(']')[0].split(',');

            _infered = true;

            types.every(getInferHelper('string'));
            if (_infered) {
                type = 'type-string-array';
            } else {
                _infered = true;
                types.every(getInferHelper('number'));
                if (_infered) {
                    type = 'type-num-array';
                } else {
                    _infered = true;
                    types.every(getInferHelper('Object'));
                    if (_infered) {
                        type = 'type-object-array';
                    }
                }
            }
            return type;
        }

        function getRenderTypeClass(type) {
            var typeClass = 'type-undetermined';
            if (type) {
                if (type.indexOf('Object') === 0) {
                    typeClass = 'type-object';
                } else if (type.indexOf('[') === 0) {
                    typeClass = inferArrayTypeClass(type);
                } else if (type.indexOf('fn') === 0) {
                    typeClass = 'type-function';
                } else if (type.indexOf('string') === 0) {
                    typeClass = "type-string";
                } else if (type.indexOf('number') === 0) {
                    typeClass = 'type-number';
                } else if (type.indexOf('bool') === 0) {
                    typeClass = 'type-boolean';
                }
            }
            return typeClass;
        }

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {jQuery.Deferred|{
         *              hints: Array.<string|jQueryObject>,
         *              match: string,
         *              selectInitial: boolean,
         *              handleWideResults: boolean}}
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var $hintObj    = $("").addClass("brackets-js-hints");
                ($hintObj).addClass(getRenderTypeClass(token.type));
                //$('' + getRenderType(token.type) + '').appendTo($hintObj).addClass("brackets-js-hints-type");
                // level indicates either variable scope or property confidence
                if (!type.property && !token.builtin && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

                if (token.guess) {
                    $hintObj.addClass("guess-hint");
                }

                // is the token a keyword?
                if (token.keyword) {
                    $hintObj.addClass("keyword-hint");
                }

                if (token.literal) {
                    $hintObj.addClass("literal-hint");
                }

                // highlight the matched portion of each hint
                if (token.stringRanges) {
                    token.stringRanges.forEach(function (item) {
                        if (item.matched) {
                            $hintObj.append($("")
                                .append(_.escape(item.text))
                                .addClass("matched-hint"));
                        } else {
                            $hintObj.append(_.escape(item.text));
                        }
                    });
                } else {
                    $hintObj.text(token.value);
                }

                $hintObj.data("token", token);

                function _appendLink() {
                    if (token.url) {
                        $('< >').appendTo($hintObj).addClass("jshint-link").attr('href', token.url).on("click", function (event) {
                            event.stopImmediatePropagation();
                            event.stopPropagation();
                        });
                    }
                }

                if (token.type) {
                    if (token.type.length > 40) {
                        _appendLink();
                        $('' + " " + token.type.split('->').join(':').trim() + '').appendTo($hintObj).addClass("jshint-description");
                    } else {
                        $('' + " " + token.type.split('->').join(':').trim() + '').appendTo($hintObj).addClass("brackets-js-hints-type-details");
                        _appendLink();
                    }
                } else {
                    if (token.keyword) {
                        $('' + " " + "keyword" + '').appendTo($hintObj).addClass("brackets-js-hints-type-details").addClass("keyword");
                    }
                }

                if (token.doc) {
                    $hintObj.attr('title', token.doc);
                    $('' + " " + token.doc.trim() + '').appendTo($hintObj).addClass("jshint-jsdoc");
                }

                $('

').appendTo($hintObj);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        trimmedQuery = _.trim(query, HintUtils.SINGLE_QUOTE + HintUtils.DOUBLE_QUOTE);

        if (hints) {
            formattedHints = formatHints(hints, trimmedQuery);
        } else {
            formattedHints = [];
        }

        return {
            hints: formattedHints,
            match: null, // the CodeHintManager should not format the results
            selectInitial: true,
            handleWideResults: hints.handleWideResults
        };
    }

    /**
     * @constructor
     */
    function JSHints() {
    }

    /**
     * determine if the cached hint information should be invalidated and re-calculated
     *
     * @param {Session} session - the active hinting session
     * @return {boolean} - true if the hints should be recalculated
     */
    JSHints.prototype.needNewHints = function (session) {
        var cursor  = session.getCursor(),
            type    = session.getType();

        return !cachedHints || !cachedCursor || !cachedType ||
            cachedCursor.line !== cursor.line ||
            type.property !== cachedType.property ||
            type.context !== cachedType.context ||
            type.showFunctionType !== cachedType.showFunctionType ||
            (type.functionCallPos && cachedType.functionCallPos &&
            type.functionCallPos.ch !== cachedType.functionCallPos.ch);
    };

    /**
     *  Cache the hints and the hint's context.
     *
     *  @param {Array.<string>} hints - array of hints
     *  @param {{line:number, ch:number}} cursor - the location where the hints
     *  were created.
     * @param {{property: boolean,
                showFunctionType:boolean,
                context: string,
                functionCallPos: {line:number, ch:number}}} type -
     *  type information about the hints
     *  @param {Object} token - CodeMirror token
     */
    function setCachedHintContext(hints, cursor, type, token) {
        cachedHints = hints;
        cachedCursor = cursor;
        cachedType = type;
        cachedToken = token;
    }

    /**
     *  Reset cached hint context.
     */
    function resetCachedHintContext() {
        cachedHints = null;
        cachedCursor = null;
        cachedType = null;
        cachedToken =  null;
    }

    /**
     *  Have conditions have changed enough to justify closing the hints popup?
     *
     * @param {Session} session - the active hinting session
     * @return {boolean} - true if the hints popup should be closed.
     */
    JSHints.prototype.shouldCloseHints = function (session) {

        // close if the token className has changed then close the hints.
        var cursor = session.getCursor(),
            token = session.getToken(cursor),
            lastToken = cachedToken;

        // if the line has changed, then close the hints
        if (!cachedCursor || cursor.line !== cachedCursor.line) {
            return true;
        }

        if (token.type === null) {
            token = session.getNextTokenOnLine(cursor);
        }

        if (lastToken && lastToken.type === null) {
            lastToken = session.getNextTokenOnLine(cachedCursor);
        }

        // Both of the tokens should never be null (happens when token is off
        // the end of the line), so one is null then close the hints.
        if (!lastToken || !token ||
                token.type !== lastToken.type) {
            return true;
        }

        // Test if one token string is a prefix of the other.
        // If one is a prefix of the other then consider it the
        // same token and don't close the hints.
        if (token.string.length >= lastToken.string.length) {
            return token.string.indexOf(lastToken.string) !== 0;
        } else {
            return lastToken.string.indexOf(token.string) !== 0;
        }
    };

    /**
     * @return {boolean} - true if the document supports inline JavaScript
     */
    function isInlineScriptSupported(document) {
        var language = LanguageManager.getLanguageForPath(document.file.fullPath).getId();
        return _inlineScriptLanguages.indexOf(language) !== -1;
    }

    function isInlineScript(editor) {
        return editor.getModeForSelection() === "javascript";
    }

    /**
     *  Create a new StringMatcher instance, if needed.
     *
     * @return {StringMatcher} - a StringMatcher instance.
     */
    function getStringMatcher() {
        if (!matcher) {
            matcher = new StringMatch.StringMatcher({
                preferPrefixMatches: true
            });
        }

        return matcher;
    }

    /**
     *  Check if a hint response is pending.
     *
     * @param {jQuery.Deferred} deferredHints - deferred hint response
     * @return {boolean} - true if deferred hints are pending, false otherwise.
     */
    function hintsArePending(deferredHints) {
        return (deferredHints && !deferredHints.hasOwnProperty("hints") &&
            deferredHints.state() === "pending");
    }

    /**
     *  Common code to get the session hints. Will get guesses if there were
     *  no completions for the query.
     *
     * @param {string} query - user text to search hints with
     *  @param {{line:number, ch:number}} cursor - the location where the hints
     *  were created.
     * @param {{property: boolean,
                 showFunctionType:boolean,
                 context: string,
                 functionCallPos: {line:number, ch:number}}} type -
     *  type information about the hints
     *  @param {Object} token - CodeMirror token
     * @param {jQuery.Deferred=} $deferredHints - existing Deferred we need to
     * resolve (optional). If not supplied a new Deferred will be created if
     * needed.
     * @return {Object + jQuery.Deferred} - hint response (immediate or
     *     deferred) as defined by the CodeHintManager API
     */
    function getSessionHints(query, cursor, type, token, $deferredHints) {

        var hintResults = session.getHints(query, getStringMatcher());
        if (hintResults.needGuesses) {
            var guessesResponse = ScopeManager.requestGuesses(session,
                session.editor.document);

            if (!$deferredHints) {
                $deferredHints = $.Deferred();
            }

            guessesResponse.done(function () {
                if (hintsArePending($deferredHints)) {
                    hintResults = session.getHints(query, getStringMatcher());
                    setCachedHintContext(hintResults.hints, cursor, type, token);
                    var hintResponse = getHintResponse(cachedHints, query, type);
                    $deferredHints.resolveWith(null, [hintResponse]);
                }
            }).fail(function () {
                if (hintsArePending($deferredHints)) {
                    $deferredHints.reject();
                }
            });

            return $deferredHints;
        } else if (hintsArePending($deferredHints)) {
            setCachedHintContext(hintResults.hints, cursor, type, token);
            var hintResponse    = getHintResponse(cachedHints, query, type);
            $deferredHints.resolveWith(null, [hintResponse]);
            return null;
        } else {
            setCachedHintContext(hintResults.hints, cursor, type, token);
            return getHintResponse(cachedHints, query, type);
        }
    }

    /**
     * Determine whether hints are available for a given editor context
     *
     * @param {Editor} editor - the current editor context
     * @param {string} key - charCode of the last pressed key
     * @return {boolean} - can the provider provide hints for this session?
     */
    JSHints.prototype.hasHints = function (editor, key) {
        if (session && HintUtils.hintableKey(key, !noHintsOnDot)) {

            if (isInlineScriptSupported(session.editor.document)) {
                if (!isInlineScript(session.editor)) {
                    return false;
                }
            }
            var cursor  = session.getCursor(),
                token   = session.getToken(cursor);

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                if (session.isFunctionName()) {
                    return false;
                }

                if (this.needNewHints(session)) {
                    resetCachedHintContext();
                    matcher = null;
                }
                return true;
            }
        }
        return false;
    };

    /**
      * Return a list of hints, possibly deferred, for the current editor
      * context
      *
      * @param {string} key - charCode of the last pressed key
      * @return {Object + jQuery.Deferred} - hint response (immediate or
      *     deferred) as defined by the CodeHintManager API
      */
    JSHints.prototype.getHints = function (key) {
        var cursor = session.getCursor(),
            token = session.getToken(cursor);

        if (token && HintUtils.hintableKey(key, !noHintsOnDot) && HintUtils.hintable(token)) {
            var type    = session.getType(),
                query   = session.getQuery();

            // If the hint context is changed and the hints are open, then
            // close the hints by returning null;
            if (CodeHintManager.isOpen() && this.shouldCloseHints(session)) {
                return null;
            }

            // Compute fresh hints if none exist, or if the session
            // type has changed since the last hint computation
            if (this.needNewHints(session)) {
                if (key) {
                    ScopeManager.handleFileChange([{from: cursor, to: cursor, text: [key]}]);
                    ignoreChange = true;
                }

                var scopeResponse   = ScopeManager.requestHints(session, session.editor.document),
                    $deferredHints  = $.Deferred(),
                    scopeSession    = session;

                scopeResponse.done(function () {
                    if (hintsArePending($deferredHints)) {
                        // Verify we are still in same session
                        if (scopeSession === session) {
                            getSessionHints(query, cursor, type, token, $deferredHints);
                        } else {
                            $deferredHints.reject();
                        }
                    }
                    scopeSession = null;
                }).fail(function () {
                    if (hintsArePending($deferredHints)) {
                        $deferredHints.reject();
                    }
                    scopeSession = null;
                });

                return $deferredHints;
            }

            if (cachedHints) {
                return getSessionHints(query, cursor, type, token);
            }
        }

        return null;
    };

    /**
     * Inserts the hint selected by the user into the current editor.
     *
     * @param {jQuery.Object} $hintObj - hint object to insert into current editor
     * @return {boolean} - should a new hinting session be requested
     *      immediately after insertion?
     */
    JSHints.prototype.insertHint = function ($hintObj) {
        var hint        = $hintObj.data("token"),
            completion  = hint.value,
            cursor      = session.getCursor(),
            query       = session.getQuery(),
            start       = {line: cursor.line, ch: cursor.ch - query.length},
            end         = {line: cursor.line, ch: cursor.ch},
            invalidPropertyName = false;

        if (session.getType().property) {
            // if we're inserting a property name, we need to make sure the
            // hint is a valid property name.
            // to check this, run the hint through Acorns tokenizer
            // it should result in one token, and that token should either be
            // a 'name' or a 'keyword', as javascript allows keywords as property names
            var tokenizer = Acorn.tokenize(completion);
            var currentToken = tokenizer();

            // the name is invalid if the hint is not a 'name' or 'keyword' token
            if (currentToken.type !== Acorn.tokTypes.name && !currentToken.type.keyword) {
                invalidPropertyName = true;
            } else {
                // check for a second token - if there is one (other than 'eof')
                // then the hint isn't a valid property name either
                currentToken = tokenizer();
                if (currentToken.type !== Acorn.tokTypes.eof) {
                    invalidPropertyName = true;
                }
            }

            if (invalidPropertyName) {
                // need to walk back to the '.' and replace
                // with '["<hint>"]
                var dotCursor = session.findPreviousDot();
                if (dotCursor) {
                    completion = "[\"" + completion + "\"]";
                    start.line = dotCursor.line;
                    start.ch = dotCursor.ch - 1;
                }
            }
        }

        // Replace the current token with the completion
        // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance
        // directly to replace the range instead of using the Document, as we should. The
        // reason is due to a flaw in our current document synchronization architecture when
        // inline editors are open.
        session.editor._codeMirror.replaceRange(completion, start, end);

        // Return false to indicate that another hinting session is not needed
        return false;
    };

    // load the extension
    AppInit.appReady(function () {

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {!Editor} editor - editor context to be initialized.
         * @param {?Editor} previousEditor - the previous editor.
         */
        function initializeSession(editor, previousEditor) {
            session = new Session(editor);
            ScopeManager.handleEditorChange(session, editor.document,
                previousEditor ? previousEditor.document : null);
            ParameterHintManager.setSession(session);
            cachedHints = null;
        }

        /*
         * Connects to the given editor, creating a new Session & adding listeners
         *
         * @param {?Editor} editor - editor context on which to listen for
         *      changes. If null, 'session' is cleared.
         * @param {?Editor} previousEditor - the previous editor
         */
        function installEditorListeners(editor, previousEditor) {
            // always clean up cached scope and hint info
            resetCachedHintContext();

            if (!jsHintsEnabled) {
                return;
            }

            if (editor && HintUtils.isSupportedLanguage(LanguageManager.getLanguageForPath(editor.document.file.fullPath).getId())) {
                initializeSession(editor, previousEditor);
                editor
                    .on(HintUtils.eventName("change"), function (event, editor, changeList) {
                        if (!ignoreChange) {
                            ScopeManager.handleFileChange(changeList);
                            ParameterHintManager.popUpHintAtOpenParen();
                        }
                        ignoreChange = false;
                    });
                ParameterHintManager.installListeners(editor);
            } else {
                session = null;
            }
        }

        /*
         * Uninstall editor change listeners
         *
         * @param {Editor} editor - editor context on which to stop listening
         *      for changes
         */
        function uninstallEditorListeners(editor) {
            if (editor) {
                editor.off(HintUtils.eventName("change"));
                ParameterHintManager.uninstallListeners(editor);
            }
        }

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            // Uninstall "languageChanged" event listeners on previous editor's document & put them on current editor's doc
            if (previous) {
                previous.document
                    .off(HintUtils.eventName("languageChanged"));
            }
            if (current) {
                current.document
                    .on(HintUtils.eventName("languageChanged"), function () {
                        // If current doc's language changed, reset our state by treating it as if the user switched to a
                        // different document altogether
                        uninstallEditorListeners(current);
                        installEditorListeners(current);
                    });
            }

            uninstallEditorListeners(previous);
            installEditorListeners(current, previous);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset,
                handleJumpResponse;

            // Only provide jump-to-definition results when cursor is in JavaScript content
            if (!session || session.editor.getModeForSelection() !== "javascript") {
                return null;
            }

            var result = new $.Deferred();

            /**
             * Make a jump-to-def request based on the session and offset passed in.
             * @param {Session} session - the session
             * @param {number} offset - the offset of where to jump from
             */
            function requestJumpToDef(session, offset) {
                var response = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

                if (response.hasOwnProperty("promise")) {
                    response.promise.done(handleJumpResponse).fail(function () {
                        result.reject();
                    });
                }
            }

            /**
             * Sets the selection to move the cursor to the result position.
             * Assumes that the editor has already changed files, if necessary.
             *
             * Additionally, this will check to see if the selection looks like an
             * assignment to a member expression - if it is, and the type is a function,
             * then we will attempt to jump to the RHS of the expression.
             *
             * 'exports.foo = foo'
             *
             * if the selection is 'foo' in 'exports.foo', then we will attempt to jump to def
             * on the rhs of the assignment.
             *
             * @param {number} start - the start of the selection
             * @param {number} end - the end of the selection
             * @param {boolean} isFunction - true if we are jumping to the source of a function def
             */
            function setJumpSelection(start, end, isFunction) {

                /**
                 * helper function to decide if the tokens on the RHS of an assignment
                 * look like an identifier, or member expr.
                 */
                function validIdOrProp(token) {
                    if (!token) {
                        return false;
                    }
                    if (token.string === ".") {
                        return true;
                    }
                    var type = token.type;
                    if (type === "variable-2" || type === "variable" || type === "property") {
                        return true;
                    }

                    return false;
                }

                var madeNewRequest = false;

                if (isFunction) {
                    // When jumping to function defs, follow the chain back
                    // to get to the original function def
                    var cursor = {line: end.line, ch: end.ch},
                        prev = session._getPreviousToken(cursor),
                        next,
                        offset;

                    // see if the selection is preceded by a '.', indicating we're in a member expr
                    if (prev.string === ".") {
                        cursor = {line: end.line, ch: end.ch};
                        next = session.getNextToken(cursor, true);
                        // check if the next token indicates an assignment
                        if (next && next.string === "=") {
                            next = session.getNextToken(cursor, true);
                            // find the last token of the identifier, or member expr
                            while (validIdOrProp(next)) {
                                offset = session.getOffsetFromCursor({line: cursor.line, ch: next.end});
                                next = session.getNextToken(cursor, false);
                            }
                            if (offset) {
                                // trigger another jump to def based on the offset of the RHS
                                requestJumpToDef(session, offset);
                                madeNewRequest = true;
                            }
                        }
                    }
                }
                // We didn't make a new jump-to-def request, so we can resolve the promise
                // and set the selection
                if (!madeNewRequest) {
                    // set the selection
                    session.editor.setSelection(start, end, true);
                    result.resolve(true);
                }
            }

            /**
             * handle processing of the completed jump-to-def request.
             * will open the appropriate file, and set the selection based
             * on the response.
             */
            handleJumpResponse = function (jumpResp) {

                if (jumpResp.resultFile) {
                    if (jumpResp.resultFile !== jumpResp.file) {
                        var resolvedPath = ScopeManager.getResolvedPath(jumpResp.resultFile);
                        if (resolvedPath) {
                            CommandManager.execute(Commands.FILE_OPEN, {fullPath: resolvedPath})
                                .done(function () {
                                    setJumpSelection(jumpResp.start, jumpResp.end, jumpResp.isFunction);
                                });
                        }
                    } else {
                        setJumpSelection(jumpResp.start, jumpResp.end, jumpResp.isFunction);
                    }
                } else {
                    result.reject();
                }
            };

            offset = session.getOffset();
            // request a jump-to-def
            requestJumpToDef(session, offset);

            return result.promise();
        }

        /*
         * Helper for QuickEdit jump-to-definition request.
         */
        function quickEditHelper() {
            var offset     = session.getCursor(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            return response;
        }

        // Register quickEditHelper.
        brackets._jsCodeHintsHelper = quickEditHelper;

        // Configuration function used for debugging
        brackets._configureJSCodeHints = setConfig;

        ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");

        // uninstall/install change listener as the active editor changes
        EditorManager.on(HintUtils.eventName("activeEditorChange"),
                handleActiveEditorChange);

        ProjectManager.on("beforeProjectClose", function () {
            ScopeManager.handleProjectClose();
        });

        ProjectManager.on("projectOpen", function () {
            ScopeManager.handleProjectOpen();
        });

        // immediately install the current editor
        installEditorListeners(EditorManager.getActiveEditor());

        // init
        EditorManager.registerJumpToDefProvider(handleJumpToDefinition);

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, HintUtils.SUPPORTED_LANGUAGES, 0);

        ParameterHintManager.addCommands();

        // for unit testing
        exports.getSession = getSession;
        exports.jsHintProvider = jsHints;
        exports.initializeSession = initializeSession;
        exports.handleJumpToDefinition = handleJumpToDefinition;
    });
});


var binary = require('../');
var test = require('tap').test;
var Stream = require('stream').Stream;

test('loop', function (t) {
    t.plan(3 * 2 + 1);

    var rs = new Stream;
    rs.readable = true;

    var ws = binary()
        .loop(function (end, vars) {
            t.strictEqual(vars, this.vars);
            this
                .word16lu('a')
                .word8u('b')
                .word8s('c')
                .tap(function (vars_) {
                    t.strictEqual(vars, vars_);
                    if (vars.c < 0) end();
                })
            ;
        })
        .tap(function (vars) {
            t.same(vars, { a : 1337, b : 55, c : -5 });
        })
    ;
    rs.pipe(ws);

    setTimeout(function () {
        rs.emit('data', new Buffer([ 2, 10, 88 ]));
    }, 10);
    setTimeout(function () {
        rs.emit('data', new Buffer([ 100, 3, 6, 242, 30 ]));
    }, 20);
    setTimeout(function () {
        rs.emit('data', new Buffer([ 60, 60, 199, 44 ]));
    }, 30);

    setTimeout(function () {
        rs.emit('data', new Buffer([ 57, 5 ]));
    }, 80);
    setTimeout(function () {
        rs.emit('data', new Buffer([ 55, 251 ]));
    }, 90);
    setTimeout(function () {
        rs.emit('end');
    }, 100);
});


"use strict"; // run code in ES5 strict mode

var pathUtil = require("path"),
    match,
    basePath;

match = __dirname.match(/(.*)[\\\/].+[\\\/]rewire[\\\/]lib[\\\/]/);
if (match === null) {
    return (/\.js$/);
} else {
    basePath = match[1];
    basePath.replace(/([\\\/])/g, "\\$1");
    return new RegExp()
}


module.exports = rimraf
rimraf.sync = rimrafSync

var path = require("path")
  , fs

try {
  // optional dependency
  fs = require("graceful-fs")
} catch (er) {
  fs = require("fs")
}

// for EMFILE handling
var timeout = 0
exports.EMFILE_MAX = 1000
exports.BUSYTRIES_MAX = 3

var isWindows = (process.platform === "win32")

function rimraf (p, cb) {
  if (!cb) throw new Error("No callback passed to rimraf()")

  var busyTries = 0
  rimraf_(p, function CB (er) {
    if (er) {
      if (er.code === "EBUSY" && busyTries < exports.BUSYTRIES_MAX) {
        busyTries ++
        var time = busyTries * 100
        // try again, with the same exact callback as this one.
        return setTimeout(function () {
          rimraf_(p, CB)
        }, time)
      }

      // this one won't happen if graceful-fs is used.
      if (er.code === "EMFILE" && timeout < exports.EMFILE_MAX) {
        return setTimeout(function () {
          rimraf_(p, CB)
        }, timeout ++)
      }

      // already gone
      if (er.code === "ENOENT") er = null
    }

    timeout = 0
    cb(er)
  })
}

// Two possible strategies.
// 1. Assume it's a file.  unlink it, then do the dir stuff on EPERM or EISDIR
// 2. Assume it's a directory.  readdir, then do the file stuff on ENOTDIR
//
// Both result in an extra syscall when you guess wrong.  However, there
// are likely far more normal files in the world than directories.  This
// is based on the assumption that a the average number of files per
// directory is >= 1.
//
// If anyone ever complains about this, then I guess the strategy could
// be made configurable somehow.  But until then, YAGNI.
function rimraf_ (p, cb) {
  fs.unlink(p, function (er) {
    if (er) {
      if (er.code === "ENOENT")
        return cb()
      if (er.code === "EPERM")
        return (isWindows) ? fixWinEPERM(p, er, cb) : rmdir(p, er, cb)
      if (er.code === "EISDIR")
        return rmdir(p, er, cb)
    }
    return cb(er)
  })
}

function fixWinEPERM (p, er, cb) {
  fs.chmod(p, 666, function (er2) {
    if (er2)
      cb(er2.code === "ENOENT" ? null : er)
    else
      fs.stat(p, function(er3, stats) {
        if (er3)
          cb(er3.code === "ENOENT" ? null : er)
        else if (stats.isDirectory())
          rmdir(p, er, cb)
        else
          fs.unlink(p, cb)
      })
  })
}

function fixWinEPERMSync (p, er, cb) {
  try {
    fs.chmodSync(p, 666)
  } catch (er2) {
    if (er2.code !== "ENOENT")
      throw er
  }

  try {
    var stats = fs.statSync(p)
  } catch (er3) {
    if (er3 !== "ENOENT")
      throw er
  }

  if (stats.isDirectory())
    rmdirSync(p, er)
  else
    fs.unlinkSync(p)
}

function rmdir (p, originalEr, cb) {
  // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS)
  // if we guessed wrong, and it's not a directory, then
  // raise the original error.
  fs.rmdir(p, function (er) {
    if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST"))
      rmkids(p, cb)
    else if (er && er.code === "ENOTDIR")
      cb(originalEr)
    else
      cb(er)
  })
}

function rmkids(p, cb) {
  fs.readdir(p, function (er, files) {
    if (er)
      return cb(er)
    var n = files.length
    if (n === 0)
      return fs.rmdir(p, cb)
    var errState
    files.forEach(function (f) {
      rimraf(path.join(p, f), function (er) {
        if (errState)
          return
        if (er)
          return cb(errState = er)
        if (--n === 0)
          fs.rmdir(p, cb)
      })
    })
  })
}

// this looks simpler, and is strictly *faster*, but will
// tie up the JavaScript thread and fail on excessively
// deep directory trees.
function rimrafSync (p) {
  try {
    fs.unlinkSync(p)
  } catch (er) {
    if (er.code === "ENOENT")
      return
    if (er.code === "EPERM")
      return isWindows ? fixWinEPERMSync(p, er) : rmdirSync(p, er)
    if (er.code !== "EISDIR")
      throw er
    rmdirSync(p, er)
  }
}

function rmdirSync (p, originalEr) {
  try {
    fs.rmdirSync(p)
  } catch (er) {
    if (er.code === "ENOENT")
      return
    if (er.code === "ENOTDIR")
      throw originalEr
    if (er.code === "ENOTEMPTY" || er.code === "EEXIST")
      rmkidsSync(p)
  }
}

function rmkidsSync (p) {
  fs.readdirSync(p).forEach(function (f) {
    rimrafSync(path.join(p, f))
  })
  fs.rmdirSync(p)
}


$('<span>' + " " + token.type.split('->').join(':').trim() + '</span>').appendTo($hintObj).addClass("brackets-js-hints-type-details");

$('<span>' + token.type.split('->').join(':').toString().trim() + '</span>').appendTo($hintObj).addClass("brackets-js-hints-type-details");

/*
 * 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.
 *
 */

.hint-width-limiter {
    width:400px;
}

span.brackets-js-hints {
    width: 400px;
    display: inline-block;
}

.brackets-js-hints.type-undetermined:before {
    color: cornflowerblue;
    content: '12';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    visibility: hidden;
    line-height: 1.2em;
}

.brackets-js-hints.type-number:before {
    color: cornflowerblue;
    content: '12';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-string:before {
    color: cornflowerblue;
    content: 'ab';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-function:before {
    color: cornflowerblue;
    content: 'fn( )';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-array:before {
    color: cornflowerblue;
    content: '[  ]';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-object:before {
    color: cornflowerblue;
    content: '{ :}';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-num-array:before {
    color: cornflowerblue;
    content: '[12]';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-string-array:before {
    color: cornflowerblue;
    content: '[ab]';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints.type-boolean:before {
    color: cornflowerblue;
    content: 'bool';
    border: 1px cornflowerblue solid;
    float:left;
    width:25px;
    text-align: center;
    margin-left:-5px;
    margin-right:5px;
    line-height: 1.2em;
}

.brackets-js-hints-type-details {
    color: #d3d3d3 !important;
    font-weight: 100;
    font-style: italic !important;
    margin-right: 5px;
    float:right;
}

.jshint-description {
    display: none;
    padding-left: 35px !important;
    padding-right:10px !important;
    color: #d4d4d4;
    word-wrap: break-word;
    white-space: normal;
    width:400px;
    box-sizing: border-box;
}

.dark .jshint-description {
    color: #ccc;
}

.jshint-jsdoc {
    display: none;
    padding-left: 28px !important;
    padding-right: 10px !important;
    color: grey;
    word-wrap: break-word;
    white-space: normal;
    width: 400px;
    box-sizing: border-box;
    float: left;
    clear: left;
    max-height: 3em;
    overflow: hidden;
}

.dark .jshint-jsdoc {
    color: #ccc;
}

.jshint-link {
    float:right;
    display:none;
    color:orangered !important;
    margin:0px !important;
    margin-right: 10px !important;
    padding:0px !important;
    font-weight: 900 !important;
}

.highlight .jshint-link {
    display:block;
}

.highlight .jshint-description {
    display: block;
    color: #6495ed !important;
}

.highlight .jshint-jsdoc {
    display: block;
}

.dark .brackets-js-hints-type-details {
    color: #696969 !important;
}

.highlight .brackets-js-hints-type-details {
    color: #6495ed !important;
    display: inline-block;
}

.brackets-js-hints-type-details.keyword {
    color: #6495ed !important;
}

.brackets-js-hints.priority-high {
    color: #486c00; /* green */
}

.brackets-js-hints.priority-medium {
    color: #0c50af; /* blue */
}

.brackets-js-hints.priority-low {
    color: #6b24bd; /* violet */
}

.brackets-js-hints.priority-lowest {
    color: #444; /* dark grey */
}

.brackets-js-hints.variable-hint {
}

.brackets-js-hints.property-hint {
}

.brackets-js-hints.literal-hint {
    color: #444; /* dark grey */
    font-family: SourceCodePro;
}

.brackets-js-hints.keyword-hint {
    font-family: SourceCodePro;
}

.brackets-js-hints.keyword-hint .matched-hint {
    font-family: SourceCodePro-Medium;
    font-weight: 500;
}

.brackets-js-hints .matched-hint {
    font-weight: 500;
}

.brackets-js-hints.guess-hint {
    font-style: italic;
}

#function-hint-container {
    display: none;

    background: #fff;
    position: absolute;
    z-index: 15;
    left: 200px;
    top: 40px;
    pointer-events: none;

    padding: 1px 6px;
    text-align: center;

    border-radius: 3px;
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.24);
}

#function-hint-container .function-hint-content {
}

.brackets-js-hints .current-parameter {
    font-weight: 500;
}

/* Dark Styles */

.dark .brackets-js-hints.priority-high {
    color: #a1f200; /* green */
}

.dark .brackets-js-hints.priority-medium {
    color: #3ff8ff; /* blue */
}

.dark .brackets-js-hints.priority-low {
    color: #e6adff; /* violet */
}

.dark .brackets-js-hints.priority-lowest {
    color: #bbb; /* dark grey */
}

.dark .brackets-js-hints.literal-hint {
    color: #bbb; /* dark grey */
    font-family: SourceCodePro;
}

.dark #function-hint-container {
    background: #000;
    border: 1px solid rgba(255,255,255,0.15);
    color: #fff;
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.24);
}