/*
 * 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, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */

/**
 * Text-editing commands that apply to whichever Editor is currently focused
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var Commands           = require("command/Commands"),
        Strings            = require("strings"),
        CommandManager     = require("command/CommandManager"),
        EditorManager      = require("editor/EditorManager"),
        StringUtils        = require("utils/StringUtils"),
        TokenUtils         = require("utils/TokenUtils");

    /**
     * List of constants
     */
    var DIRECTION_UP    = -1;
    var DIRECTION_DOWN  = +1;

    /**
     * @private
     * Creates regular expressions for multiple line comment prefixes
     * @param {!Array.<string>} prefixes - the line comment prefixes
     * @return {Array.<RegExp>}
     */
    function _createLineExpressions(prefixes) {
        var lineExp = [];
        prefixes.forEach(function (prefix) {
            lineExp.push(new RegExp("^\\s*" + StringUtils.regexEscape(prefix)));
        });
        return lineExp;
    }

    /**
     * @private
     * Returns true if any regular expression matches the given string
     * @param {!string} string - where to look
     * @param {!Array.<RegExp>} expressions - what to look
     * @return {boolean}
     */
    function _matchExpressions(string, expressions) {
        return expressions.some(function (exp) {
            return string.match(exp);
        });
    }

    /**
     * @private
     * Returns the line comment prefix that best matches the string. Since there might be line comment prefixes
     * that are prefixes of other line comment prefixes, it searches throught all and returns the longest line
     * comment prefix that matches the string.
     * @param {!string} string - where to look
     * @param {!Array.<RegExp>} expressions - the line comment regular expressions
     * @param {!Array.<string>} prefixes - the line comment prefixes
     * @return {string}
     */
    function _getLinePrefix(string, expressions, prefixes) {
        var result = null;
        expressions.forEach(function (exp, index) {
            if (string.match(exp) && ((result && result.length < prefixes[index].length) || !result)) {
                result = prefixes[index];
            }
        });
        return result;
    }

    /**
     * @private
     * Searchs for an uncommented line between startLine and endLine
     * @param {!Editor} editor
     * @param {!number} startLine - valid line inside the document
     * @param {!number} endLine - valid line inside the document
     * @param {!Array.<string>} lineExp - an array of line comment prefixes regular expressions
     * @return {boolean} true if there is at least one uncommented line
     */
    function _containsUncommented(editor, startLine, endLine, lineExp) {
        var containsUncommented = false;
        var i;
        var line;

        for (i = startLine; i <= endLine; i++) {
            line = editor.document.getLine(i);
            // A line is commented out if it starts with 0-N whitespace chars, then a line comment prefix
            if (line.match(/\S/) && !_matchExpressions(line, lineExp)) {
                containsUncommented = true;
                break;
            }
        }
        return containsUncommented;
    }

    /**
     * Add or remove line-comment tokens to all the lines in the selected range, preserving selection
     * and cursor position. Applies to currently focused Editor.
     *
     * If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment
     * out. Commenting out adds the prefix at column 0 of every line. Uncommenting removes the first prefix
     * on each line (if any - empty lines might not have one).
     *
     * @param {!Editor} editor
     * @param {!Array.<string>} prefixes, e.g. ["//"]
     */
    function lineCommentPrefix(editor, prefixes) {
        var doc       = editor.document,
            sel       = editor.getSelection(),
            startLine = sel.start.line,
            endLine   = sel.end.line,
            lineExp   = _createLineExpressions(prefixes);

        // Is a range of text selected? (vs just an insertion pt)
        var hasSelection = (startLine !== endLine) || (sel.start.ch !== sel.end.ch);

        // In full-line selection, cursor pos is start of next line - but don't want to modify that line
        if (sel.end.ch === 0 && hasSelection) {
            endLine--;
        }

        // Decide if we're commenting vs. un-commenting
        // Are there any non-blank lines that aren't commented out? (We ignore blank lines because
        // some editors like Sublime don't comment them out)
        var containsUncommented = _containsUncommented(editor, startLine, endLine, lineExp);
        var i;
        var line;
        var prefix;
        var commentI;
        var updateSelection = false;

        // Make the edit
        doc.batchOperation(function () {

            if (containsUncommented) {
                // Comment out - prepend the first prefix to each line
                for (i = startLine; i <= endLine; i++) {
                    doc.replaceRange(prefixes[0], {line: i, ch: 0});
                }

                // Make sure selection includes the prefix that was added at start of range
                if (sel.start.ch === 0 && hasSelection) {
                    updateSelection = true;
                }

            } else {
                // Uncomment - remove the prefix on each line (if any)
                for (i = startLine; i <= endLine; i++) {
                    line   = doc.getLine(i);
                    prefix = _getLinePrefix(line, lineExp, prefixes);

                    if (prefix) {
                        commentI = line.indexOf(prefix);
                        doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + prefix.length});
                    }
                }
            }
        });

        // Update the selection after the document batch so it's not blown away on resynchronization
        // if this editor is not the master editor.
        if (updateSelection) {
            // use *current* selection end, which has been updated for our text insertions
            editor.setSelection({line: startLine, ch: 0}, editor.getSelection().end);
        }
    }

    /**
     * @private
     * Moves the token context to the token that starts the block-comment. Ctx starts in a block-comment.
     * Returns the position of the prefix or null if gets to the start of the document and didn't found it.
     * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context
     * @param {!RegExp} prefixExp - a valid regular expression
     * @return {?{line: number, ch: number}}
     */
    function _findCommentStart(ctx, prefixExp) {
        var result = true;

        while (result && !ctx.token.string.match(prefixExp)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
        }
        return result ? {line: ctx.pos.line, ch: ctx.token.start} : null;
    }

    /**
     * @private
     * Moves the token context to the token that ends the block-comment. Ctx starts in a block-comment.
     * Returns the position of the sufix or null if gets to the end of the document and didn't found it.
     * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context
     * @param {!RegExp} suffixExp - a valid regular expression
     * @param {!number} suffixLen - length of the suffix
     * @return {?{line: number, ch: number}}
     */
    function _findCommentEnd(ctx, suffixExp, suffixLen) {
        var result = true;

        while (result && !ctx.token.string.match(suffixExp)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
        }
        return result ? {line: ctx.pos.line, ch: ctx.token.end - suffixLen} : null;
    }

    /**
     * @private
     * Moves the token context to the next block-comment if there is one before end.
     * @param {!{editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}}} ctx - token context
     * @param {!{line: number, ch: number}} end - where to stop searching
     * @param {!RegExp} prefixExp - a valid regular expression
     * @return {boolean} - true if it found a block-comment
     */
    function _findNextBlockComment(ctx, end, prefixExp) {
        var index  = ctx.editor.indexFromPos(end),
            inside = ctx.editor.indexFromPos(ctx.pos) <= index,
            result = true;

        while (result && inside && !ctx.token.string.match(prefixExp)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
            inside = ctx.editor.indexFromPos(ctx.pos) <= index;
        }
        return result && inside && !!ctx.token.string.match(prefixExp);
    }

    /**
     * Add or remove block-comment tokens to the selection, preserving selection
     * and cursor position. Applies to the currently focused Editor.
     *
     * If the selection is inside a block-comment or one block-comment is inside or partially
     * inside the selection we uncomment; otherwise we comment out.
     * Commenting out adds the prefix before the selection and the suffix after.
     * Uncommenting removes them.
     *
     * If slashComment is true and the start or end of the selection is inside a line-comment it
     * will try to do a line uncomment if is not actually inside a bigger block comment and all
     * the lines in the selection are line-commented.
     *
     * @param {!Editor} editor
     * @param {!string} prefix, e.g. "<!--"
     * @param {!string} suffix, e.g. "-->"
     * @param {!Array.<string>} linePrefixes, e.g. ["//"]
     */
    function blockCommentPrefixSuffix(editor, prefix, suffix, linePrefixes) {

        var doc            = editor.document,
            sel            = editor.getSelection(),
            ctx            = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.start.line, ch: sel.start.ch}),
            startCtx       = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.start.line, ch: sel.start.ch}),
            endCtx         = TokenUtils.getInitialContext(editor._codeMirror, {line: sel.end.line, ch: sel.end.ch}),
            prefixExp      = new RegExp("^" + StringUtils.regexEscape(prefix), "g"),
            suffixExp      = new RegExp(StringUtils.regexEscape(suffix) + "$", "g"),
            lineExp        = _createLineExpressions(linePrefixes),
            prefixPos      = null,
            suffixPos      = null,
            canComment     = false,
            invalidComment = false,
            lineUncomment  = false,
            newSelection;

        var result, text, line;

        // Move the context to the first non-empty token.
        if (!ctx.token.type && ctx.token.string.trim().length === 0) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
        }

        // Check if we should just do a line uncomment (if all lines in the selection are commented).
        if (lineExp.length && (_matchExpressions(ctx.token.string, lineExp) || _matchExpressions(endCtx.token.string, lineExp))) {
            var startCtxIndex = editor.indexFromPos({line: ctx.pos.line, ch: ctx.token.start});
            var endCtxIndex   = editor.indexFromPos({line: endCtx.pos.line, ch: endCtx.token.start + endCtx.token.string.length});

            // Find if we aren't actually inside a block-comment
            result = true;
            while (result && _matchExpressions(ctx.token.string, lineExp)) {
                result = TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx);
            }

            // If we aren't in a block-comment.
            if (!result || ctx.token.type !== "comment" || ctx.token.string.match(suffixExp)) {
                // Is a range of text selected? (vs just an insertion pt)
                var hasSelection = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch);

                // In full-line selection, cursor pos is start of next line - but don't want to modify that line
                var endLine = sel.end.line;
                if (sel.end.ch === 0 && hasSelection) {
                    endLine--;
                }

                // Find if all the lines are line-commented.
                if (!_containsUncommented(editor, sel.start.line, endLine, lineExp)) {
                    lineUncomment = true;

                // Block-comment in all the other cases
                } else {
                    canComment = true;
                }
            } else {
                prefixPos = _findCommentStart(startCtx, prefixExp);
                suffixPos = _findCommentEnd(startCtx, suffixExp, suffix.length);
            }

        // If we are in a selection starting and ending in invalid tokens and with no content (not considering spaces),
        // find if we are inside a block-comment.
        } else if (startCtx.token.type === null && endCtx.token.type === null &&
                !editor.posWithinRange(ctx.pos, startCtx.pos, endCtx.pos, true)) {
            result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, startCtx);

            // We found a comment, find the start and end and check if the selection is inside the block-comment.
            if (startCtx.token.type === "comment") {
                prefixPos = _findCommentStart(startCtx, prefixExp);
                suffixPos = _findCommentEnd(startCtx, suffixExp, suffix.length);

                if (prefixPos !== null && suffix !== null && !editor.posWithinRange(sel.start, prefixPos, suffixPos, true)) {
                    canComment = true;
                }
            } else {
                canComment = true;
            }

        // If the start is inside a comment, find the prefix and suffix positions.
        } else if (ctx.token.type === "comment") {
            prefixPos = _findCommentStart(ctx, prefixExp);
            suffixPos = _findCommentEnd(ctx, suffixExp, suffix.length);

        // If not try to find the first comment inside the selection.
        } else {
            result = _findNextBlockComment(ctx, sel.end, prefixExp);

            // If nothing was found is ok to comment.
            if (!result) {
                canComment = true;
            } else {
                if (!ctx.token.string.match(prefixExp)) {
                    prefixPos = _findCommentStart(ctx, prefixExp);
                } else {
                    prefixPos = {line: ctx.pos.line, ch: ctx.token.start};
                }
                suffixPos = _findCommentEnd(ctx, suffixExp, suffix.length);
            }
        }

        // Search if there is another comment in the selection. Do nothing if there is one.
        if (!canComment && !invalidComment && !lineUncomment && suffixPos) {
            var start = {line: suffixPos.line, ch: suffixPos.ch + suffix.length + 1};
            if (editor.posWithinRange(start, sel.start, sel.end, true)) {
                // Start searching at the next token, if there is one.
                result = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) &&
                         _findNextBlockComment(ctx, sel.end, prefixExp);

                if (result) {
                    invalidComment = true;
                }
            }
        }

        // Make the edit
        if (invalidComment) {
            return;

        } else if (lineUncomment) {
            lineCommentPrefix(editor, linePrefixes);

        } else {
            doc.batchOperation(function () {

                if (canComment) {
                    // Comment out - add the suffix to the start and the prefix to the end.
                    var completeLineSel = sel.start.ch === 0 && sel.end.ch === 0 && sel.start.line < sel.end.line;
                    if (completeLineSel) {
                        doc.replaceRange(suffix + "\n", sel.end);
                        doc.replaceRange(prefix + "\n", sel.start);
                    } else {
                        doc.replaceRange(suffix, sel.end);
                        doc.replaceRange(prefix, sel.start);
                    }

                    // Correct the selection.
                    if (completeLineSel) {
                        newSelection = {start: {line: sel.start.line + 1, ch: 0}, end: {line: sel.end.line + 1, ch: 0}};
                    } else {
                        var newSelStart = {line: sel.start.line, ch: sel.start.ch + prefix.length};
                        if (sel.start.line === sel.end.line) {
                            newSelection = {start: newSelStart, end: {line: sel.end.line, ch: sel.end.ch + prefix.length}};
                        } else {
                            newSelection = {start: newSelStart, end: {line: sel.end.line, ch: sel.end.ch}};
                        }
                    }

                // Uncomment - remove prefix and suffix.
                } else {
                    // Find if the prefix and suffix are at the ch 0 and if they are the only thing in the line.
                    // If both are found we assume that a complete line selection comment added new lines, so we remove them.
                    var prefixAtStart = false, suffixAtStart = false;

                    line = doc.getLine(prefixPos.line).trim();
                    prefixAtStart = prefixPos.ch === 0 && prefix.length === line.length;
                    if (suffixPos) {
                        line = doc.getLine(suffixPos.line).trim();
                        suffixAtStart = suffixPos.ch === 0 && suffix.length === line.length;
                    }

                    // Remove the suffix if there is one
                    if (suffixPos) {
                        if (prefixAtStart && suffixAtStart) {
                            doc.replaceRange("", suffixPos, {line: suffixPos.line + 1, ch: 0});
                        } else {
                            doc.replaceRange("", suffixPos, {line: suffixPos.line, ch: suffixPos.ch + suffix.length});
                        }
                    }

                    // Remove the prefix
                    if (prefixAtStart && suffixAtStart) {
                        doc.replaceRange("", prefixPos, {line: prefixPos.line + 1, ch: 0});
                    } else {
                        doc.replaceRange("", prefixPos, {line: prefixPos.line, ch: prefixPos.ch + prefix.length});
                    }
                }
            });

            // Update the selection after the document batch so it's not blown away on resynchronization
            // if this editor is not the master editor.
            if (newSelection) {
                editor.setSelection(newSelection.start, newSelection.end);
            }
        }
    }

    /**
     * Add or remove block-comment tokens to the selection, preserving selection
     * and cursor position. Applies to the currently focused Editor.
     *
     * The implementation uses blockCommentPrefixSuffix, with the exception of the case where
     * there is no selection on a uncommented and not empty line. In this case the whole lines gets
     * commented in a block-comment.
     *
     * @param {!Editor} editor
     * @param {!String} prefix
     * @param {!String} suffix
     */
    function lineCommentPrefixSuffix(editor, prefix, suffix) {
        var sel             = editor.getSelection(),
            selStart        = sel.start,
            selEnd          = sel.end,
            prefixExp       = new RegExp("^" + StringUtils.regexEscape(prefix), "g"),
            isLineSelection = sel.start.ch === 0 && sel.end.ch === 0 && sel.start.line !== sel.end.line,
            isMultipleLine  = sel.start.line !== sel.end.line,
            lineLength      = editor.document.getLine(sel.start.line).length;

        // Line selections already behave like we want to
        if (!isLineSelection) {
            // For a multiple line selection transform it to a multiple whole line selection
            if (isMultipleLine) {
                selStart = {line: sel.start.line, ch: 0};
                selEnd   = {line: sel.end.line + 1, ch: 0};

            // For one line selections, just start at column 0 and end at the end of the line
            } else {
                selStart = {line: sel.start.line, ch: 0};
                selEnd   = {line: sel.end.line, ch: lineLength};
            }
        }

        // If the selection includes a comment or is already a line selection, delegate to Block-Comment
        var ctx       = TokenUtils.getInitialContext(editor._codeMirror, {line: selStart.line, ch: selStart.ch});
        var result    = TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx);
        var className = ctx.token.type;
        result        = result && _findNextBlockComment(ctx, selEnd, prefixExp);

        if (className === "comment" || result || isLineSelection) {
            blockCommentPrefixSuffix(editor, prefix, suffix, []);
        } else {
            // Set the new selection and comment it
            editor.setSelection(selStart, selEnd);
            blockCommentPrefixSuffix(editor, prefix, suffix, []);

            // Restore the old selection taking into account the prefix change
            if (isMultipleLine) {
                sel.start.line++;
                sel.end.line++;
            } else {
                sel.start.ch += prefix.length;
                sel.end.ch += prefix.length;
            }
            editor.setSelection(sel.start, sel.end);
        }
    }

    /**
     * Invokes a language-specific block-comment/uncomment handler
     * @param {?Editor} editor If unspecified, applies to the currently focused editor
     */
    function blockComment(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var language = editor.getLanguageForSelection();

        if (language.hasBlockCommentSyntax()) {
            // getLineCommentPrefixes always return an array, and will be empty if no line comment syntax is defined
            blockCommentPrefixSuffix(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix(), language.getLineCommentPrefixes());
        }
    }

    /**
     * Invokes a language-specific line-comment/uncomment handler
     * @param {?Editor} editor If unspecified, applies to the currently focused editor
     */
    function lineComment(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var language = editor.getLanguageForSelection();

        if (language.hasLineCommentSyntax()) {
            lineCommentPrefix(editor, language.getLineCommentPrefixes());
        } else if (language.hasBlockCommentSyntax()) {
            lineCommentPrefixSuffix(editor, language.getBlockCommentPrefix(), language.getBlockCommentSuffix());
        }
    }

    /**
     * Duplicates the selected text, or current line if no selection. The cursor/selection is left
     * on the second copy.
     */
    function duplicateText(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var sel = editor.getSelection(),
            hasSelection = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch),
            delimiter = "";

        if (!hasSelection) {
            sel.start.ch = 0;
            sel.end = {line: sel.start.line + 1, ch: 0};
            if (sel.end.line === editor.lineCount()) {
                delimiter = "\n";
            }
        }

        // Make the edit
        var doc = editor.document;

        var selectedText = doc.getRange(sel.start, sel.end) + delimiter;
        doc.replaceRange(selectedText, sel.start);
    }

    /**
     * Deletes the current line if there is no selection or the lines for the selection
     * (removing the end of line too)
     */
    function deleteCurrentLines(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var from,
            to,
            sel = editor.getSelection(),
            doc = editor.document;

        from = {line: sel.start.line, ch: 0};
        to = {line: sel.end.line + 1, ch: 0};
        if (to.line === editor.getLastVisibleLine() + 1) {
            // Instead of deleting the newline after the last line, delete the newline
            // before the first line--unless this is the entire visible content of the editor,
            // in which case just delete the line content.
            if (from.line > editor.getFirstVisibleLine()) {
                from.line -= 1;
                from.ch = doc.getLine(from.line).length;
            }
            to.line -= 1;
            to.ch = doc.getLine(to.line).length;
        }

        doc.replaceRange("", from, to);
    }

    /**
     * Moves the selected text, or current line if no selection. The cursor/selection
     * moves with the line/lines.
     * @param {Editor} editor - target editor
     * @param {Number} direction - direction of the move (-1,+1) => (Up,Down)
     */
    function moveLine(editor, direction) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var doc = editor.document,
            sel = editor.getSelection(),
            originalSel    = editor.getSelection(),
            hasSelection   = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch),
            isInlineWidget = !!EditorManager.getFocusedInlineWidget(),
            firstLine      = editor.getFirstVisibleLine(),
            lastLine       = editor.getLastVisibleLine(),
            totalLines     = editor.lineCount(),
            lineLength     = 0;

        sel.start.ch = 0;
        // The end of the selection becomes the start of the next line, if it isn't already
        if (!hasSelection || sel.end.ch !== 0) {
            sel.end = {line: sel.end.line + 1, ch: 0};
        }

        // Make the move
        switch (direction) {
        case DIRECTION_UP:
            if (sel.start.line !== firstLine) {
                doc.batchOperation(function () {
                    var prevText = doc.getRange({ line: sel.start.line - 1, ch: 0 }, sel.start);

                    if (sel.end.line === lastLine + 1) {
                        if (isInlineWidget) {
                            prevText   = prevText.substring(0, prevText.length - 1);
                            lineLength = doc.getLine(sel.end.line - 1).length;
                            doc.replaceRange("\n", { line: sel.end.line - 1, ch: lineLength });
                        } else {
                            prevText = "\n" + prevText.substring(0, prevText.length - 1);
                        }
                    }

                    doc.replaceRange("", { line: sel.start.line - 1, ch: 0 }, sel.start);
                    doc.replaceRange(prevText, { line: sel.end.line - 1, ch: 0 });

                    // Make sure CodeMirror hasn't expanded the selection to include
                    // the line we inserted below.
                    originalSel.start.line--;
                    originalSel.end.line--;
                });

                // Update the selection after the document batch so it's not blown away on resynchronization
                // if this editor is not the master editor.
                editor.setSelection(originalSel.start, originalSel.end);
            }
            break;
        case DIRECTION_DOWN:
            if (sel.end.line <= lastLine) {
                doc.batchOperation(function () {
                    var nextText      = doc.getRange(sel.end, { line: sel.end.line + 1, ch: 0 }),
                        deletionStart = sel.end;

                    if (sel.end.line === lastLine) {
                        if (isInlineWidget) {
                            if (sel.end.line === totalLines - 1) {
                                nextText += "\n";
                            }
                            lineLength = doc.getLine(sel.end.line - 1).length;
                            doc.replaceRange("\n", { line: sel.end.line, ch: doc.getLine(sel.end.line).length });
                        } else {
                            nextText     += "\n";
                            deletionStart = { line: sel.end.line - 1, ch: doc.getLine(sel.end.line - 1).length };
                        }
                    }

                    doc.replaceRange("", deletionStart, { line: sel.end.line + 1, ch: 0 });
                    if (lineLength) {
                        doc.replaceRange("", { line: sel.end.line - 1, ch: lineLength }, { line: sel.end.line, ch: 0 });
                    }
                    doc.replaceRange(nextText, { line: sel.start.line, ch: 0 });
                });
            }
            break;
        }
    }

    /**
     * Moves the selected text, or current line if no selection, one line up. The cursor/selection
     * moves with the line/lines.
     */
    function moveLineUp(editor) {
        moveLine(editor, DIRECTION_UP);
    }

    /**
     * Moves the selected text, or current line if no selection, one line down. The cursor/selection
     * moves with the line/lines.
     */
    function moveLineDown(editor) {
        moveLine(editor, DIRECTION_DOWN);
    }

    /**
     * Inserts a new and smart indented line above/below the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     * @param {Number} direction - direction where to place the new line (-1,+1) => (Up,Down)
     */
    function openLine(editor, direction) {
        editor = editor || EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        var sel            = editor.getSelection(),
            hasSelection   = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch),
            isInlineWidget = !!EditorManager.getFocusedInlineWidget(),
            lastLine       = editor.getLastVisibleLine(),
            cm             = editor._codeMirror,
            doc            = editor.document,
            line;

        // Insert the new line
        switch (direction) {
        case DIRECTION_UP:
            line = sel.start.line;
            break;
        case DIRECTION_DOWN:
            line = sel.end.line;
            if (!(hasSelection && sel.end.ch === 0)) {
                // If not linewise selection
                line++;
            }
            break;
        }

        if (line > lastLine && isInlineWidget) {
            doc.replaceRange("\n", {line: line - 1, ch: doc.getLine(line - 1).length}, null, "+input");
        } else {
            doc.replaceRange("\n", {line: line, ch: 0}, null, "+input");
        }
        cm.indentLine(line, "smart", false);
        editor.setSelection({line: line, ch: null});
    }

    /**
     * Inserts a new and smart indented line above the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     */
    function openLineAbove(editor) {
        openLine(editor, DIRECTION_UP);
    }

    /**
     * Inserts a new and smart indented line below the selected text, or current line if no selection.
     * The cursor is moved in the new line.
     * @param {Editor} editor - target editor
     */
    function openLineBelow(editor) {
        openLine(editor, DIRECTION_DOWN);
    }

    /**
     * Indent a line of text if no selection. Otherwise, indent all lines in selection.
     */
    function indentText() {
        var editor = EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        editor._codeMirror.execCommand("indentMore");
    }

    /**
     * Unindent a line of text if no selection. Otherwise, unindent all lines in selection.
     */
    function unidentText() {
        var editor = EditorManager.getFocusedEditor();
        if (!editor) {
            return;
        }

        editor._codeMirror.execCommand("indentLess");
    }

    function selectLine(editor) {
        editor = editor || EditorManager.getFocusedEditor();
        if (editor) {
            var sel  = editor.getSelection();
            var from = {line: sel.start.line, ch: 0};
            var to   = {line: sel.end.line + 1, ch: 0};

            if (to.line === editor.getLastVisibleLine() + 1) {
                // Last line: select to end of line instead of start of (hidden/nonexistent) following line,
                // which due to how CM clips coords would only work some of the time
                to.line -= 1;
                to.ch = editor.document.getLine(to.line).length;
            }

            editor.setSelection(from, to);
        }
    }

    function handleUndoRedo(operation) {
        var editor = EditorManager.getFocusedEditor();
        var result = new $.Deferred();

        if (editor) {
            editor[operation]();
            result.resolve();
        } else {
            result.reject();
        }

        return result.promise();
    }

    function handleUndo() {
        return handleUndoRedo("undo");
    }

    function handleRedo() {
        return handleUndoRedo("redo");
    }

    /**
     * Special command handler that just ignores the command. This is used for Cut, Copy, and Paste.
     * These menu items are handled natively, but need to be registered in our JavaScript code so the
     * menu items can be created.
     */
    function ignoreCommand() {
        // Do nothing. The shell will call the native handler for the command.
        return (new $.Deferred()).reject().promise();
    }

	function _handleSelectAll() {
        var result = new $.Deferred(),
            editor = EditorManager.getFocusedEditor();

        if (editor) {
            editor.selectAllNoScroll();
            result.resolve();
        } else {
            result.reject();    // command not handled
        }

        return result.promise();
    }

    // Register commands
    CommandManager.register(Strings.CMD_INDENT,           Commands.EDIT_INDENT,           indentText);
    CommandManager.register(Strings.CMD_UNINDENT,         Commands.EDIT_UNINDENT,         unidentText);
    CommandManager.register(Strings.CMD_COMMENT,          Commands.EDIT_LINE_COMMENT,     lineComment);
    CommandManager.register(Strings.CMD_BLOCK_COMMENT,    Commands.EDIT_BLOCK_COMMENT,    blockComment);
    CommandManager.register(Strings.CMD_DUPLICATE,        Commands.EDIT_DUPLICATE,        duplicateText);
    CommandManager.register(Strings.CMD_DELETE_LINES,     Commands.EDIT_DELETE_LINES,     deleteCurrentLines);
    CommandManager.register(Strings.CMD_LINE_UP,          Commands.EDIT_LINE_UP,          moveLineUp);
    CommandManager.register(Strings.CMD_LINE_DOWN,        Commands.EDIT_LINE_DOWN,        moveLineDown);
    CommandManager.register(Strings.CMD_OPEN_LINE_ABOVE,  Commands.EDIT_OPEN_LINE_ABOVE,  openLineAbove);
    CommandManager.register(Strings.CMD_OPEN_LINE_BELOW,  Commands.EDIT_OPEN_LINE_BELOW,  openLineBelow);
    CommandManager.register(Strings.CMD_SELECT_LINE,      Commands.EDIT_SELECT_LINE,      selectLine);

    CommandManager.register(Strings.CMD_UNDO,             Commands.EDIT_UNDO,             handleUndo);
    CommandManager.register(Strings.CMD_REDO,             Commands.EDIT_REDO,             handleRedo);
    CommandManager.register(Strings.CMD_CUT,              Commands.EDIT_CUT,              ignoreCommand);
    CommandManager.register(Strings.CMD_COPY,             Commands.EDIT_COPY,             ignoreCommand);
    CommandManager.register(Strings.CMD_PASTE,            Commands.EDIT_PASTE,            ignoreCommand);
    CommandManager.register(Strings.CMD_SELECT_ALL,       Commands.EDIT_SELECT_ALL,       _handleSelectAll);
});


keep keep keep replace replace replace keep keep keep keep

html {
    background-color: #eeeeee;
}

body {
    margin: 0 auto;
    padding: 2em;
    max-width: 800px;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5em;
    color: #333333;
    background-color: #ffffff;
    -webkit-box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
    -moz-box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
    box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
}

h1, h2, h3, h4, h5, h6 {
    line-height: 1.3em;
}

samp
{
    /* hide <samp> from the browser so we can show cool features in Edge Code */
    display: none;
}

img
{
    border: 1px solid black;
    background-color: dimgray;
    padding: 10px;
    margin: 10px 0;
    max-width: 95%;
}


mime

A comprehensive library for mime-type mapping

# mime

Comprehensive MIME type mapping API. Includes all 600+ types and 800+ extensions defined by the Apache project, plus additional types submitted by the node.js community.

## Install

Install with [npm](http://github.com/isaacs/npm):

    npm install mime

## API - Queries

### mime.lookup(path)
Get the mime type associated with a file. Performs a case-insensitive lookup using the extension in `path` (the substring after the last '/' or '.').  E.g.

    var mime = require('mime');

    mime.lookup('/path/to/file.txt');         // => 'text/plain'
    mime.lookup('file.txt');                  // => 'text/plain'
    mime.lookup('.TXT');                      // => 'text/plain'
    mime.lookup('htm');                       // => 'text/html'

### mime.extension(type)
Get the default extension for `type`

    mime.extension('text/html');                 // => 'html'
    mime.extension('application/octet-stream');  // => 'bin'

### mime.charsets.lookup()

Map mime-type to charset

    mime.charsets.lookup('text/plain');        // => 'UTF-8'

(The logic for charset lookups is pretty rudimentary.  Feel free to suggest improvements.)

## API - Defining Custom Types

The following APIs allow you to add your own type mappings within your project.  If you feel a type should be included as part of node-mime, see [requesting new types](https://github.com/bentomas/node-mime/wiki/Requesting-New-Types).

### mime.define()

Add custom mime/extension mappings

    mime.define({
        'text/x-some-format': ['x-sf', 'x-sft', 'x-sfml'],
        'application/x-my-type': ['x-mt', 'x-mtt'],
        // etc ...
    });

    mime.lookup('x-sft');                 // => 'text/x-some-format'

The first entry in the extensions array is returned by `mime.extension()`. E.g.

    mime.extension('text/x-some-format'); // => 'x-sf'

### mime.load(filepath)

Load mappings from an Apache ".types" format file

    mime.load('./my_project.types');

The .types file format is simple -  See the `types` dir for examples.


// Kick off two index requests in parallel

var promise1 = FileIndexManager.getFileInfoList("all")

/*
 * 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, browser: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, $, brackets, jasmine, describe, it, expect, beforeEach, afterEach, waitsFor, waits, waitsForDone, runs */
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"),
        Editor              = require("editor/Editor").Editor,
        EditorManager       = require("editor/EditorManager"),
        PanelManager        = require("view/PanelManager"),
        ExtensionLoader     = require("utils/ExtensionLoader"),
        UrlParams           = require("utils/UrlParams").UrlParams,
        LanguageManager     = require("language/LanguageManager");

    var TEST_PREFERENCES_KEY    = "com.adobe.brackets.test.preferences",
        OPEN_TAG                = "{{",
        CLOSE_TAG               = "}}",
        RE_MARKER               = /\{\{(\d+)\}\}/g,
        _testWindow,
        _doLoadExtensions,
        nfs;

    /**
     * Resolves a path string to a FileEntry or DirectoryEntry
     * @param {!string} path Path to a file or directory
     * @return {$.Promise} A promise resolved when the file/directory is found or
     *     rejected when any error occurs.
     */
    function resolveNativeFileSystemPath(path) {
        var deferred = new $.Deferred();

        NativeFileSystem.resolveNativeFileSystemPath(
            path,
            function success(entry) {
                deferred.resolve(entry);
            },
            function error(domError) {
                deferred.reject();
            }
        );

        return deferred.promise();
    }

    /**
     * Get or create a NativeFileSystem rooted at the system root.
     * @return {$.Promise} A promise resolved when the native file system is found or rejected when an error occurs.
     */
    function getRoot() {
        var deferred = new $.Deferred();

        if (nfs) {
            deferred.resolve(nfs.root);
        }

        resolveNativeFileSystemPath("/").then(deferred.resolve, deferred.reject);

        return deferred.promise();
    }

    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;
    }

    /**
     * Get the temporary unit test project path. Use this path for unit tests that need to modify files on disk.
     * @return {$.string} Path to the temporary unit test project
     */
    function getTempDirectory() {
        return getTestPath("/temp");
    }

    /**
     * Create the temporary unit test project directory.
     */
    function createTempDirectory() {
        var deferred = new $.Deferred();

        runs(function () {
            brackets.fs.makedir(getTempDirectory(), 0, function (err) {
                if (err && err !== brackets.fs.ERR_FILE_EXISTS) {
                    deferred.reject(err);
                } else {
                    deferred.resolve();
                }
            });
        });

        waitsForDone(deferred, "Create temp directory", 500);
    }

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

    /**
     * Utility for tests that wait on a Promise to complete. Placed in the global namespace so it can be used
     * similarly to the standard Jasmine waitsFor(). Unlike waitsFor(), must be called from INSIDE
     * the runs() that generates the promise.
     * @param {$.Promise} promise
     * @param {string} operationName  Name used for timeout error message
     */
    window.waitsForDone = function (promise, operationName, timeout) {
        timeout = timeout || 1000;
        expect(promise).toBeTruthy();
        waitsFor(function () {
            return promise.state() === "resolved";
        }, "success " + operationName, timeout);
    };

    /**
     * Utility for tests that waits on a Promise to fail. Placed in the global namespace so it can be used
     * similarly to the standards Jasmine waitsFor(). Unlike waitsFor(), must be called from INSIDE
     * the runs() that generates the promise.
     * @param {$.Promise} promise
     * @param {string} operationName  Name used for timeout error message
     */
    window.waitsForFail = function (promise, operationName, timeout) {
        timeout = timeout || 1000;
        expect(promise).toBeTruthy();
        waitsFor(function () {
            return promise.state() === "rejected";
        }, "failure " + operationName, timeout);
    };

    /**
     * Returns a Document suitable for use with an Editor in isolation, but that can be registered with
     * DocumentManager via addRef() so it is maintained for global updates like name and language changes.
     *
     * Like a normal Document, if you cause an addRef() on this you MUST call releaseRef() later.
     *
     * @param {!{language:?string, filename:?string, content:?string }} options
     * Language defaults to JavaScript, filename defaults to a placeholder name, and
     * content defaults to "".
     */
    function createMockActiveDocument(options) {
        var language    = options.language || LanguageManager.getLanguage("javascript"),
            filename    = options.filename || "_unitTestDummyFile_" + Date.now() + "." + language._fileExtensions[0],
            content     = options.content || "";

        // Use unique filename to avoid collissions in open documents list
        var dummyFile = new NativeFileSystem.FileEntry(filename);
        var docToShim = new DocumentManager.Document(dummyFile, new Date(), content);

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

            // 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;
    }

    /**
     * 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.
     *
     * Unlike a real Document, does NOT need to be explicitly cleaned up.
     *
     * @param {?string} initialContent  Defaults to ""
     * @param {?string} languageId      Defaults to JavaScript
     */
    function createMockDocument(initialContent, languageId) {
        var language    = LanguageManager.getLanguage(languageId) || LanguageManager.getLanguage("javascript"),
            options     = { language: language, content: initialContent },
            docToShim   = createMockActiveDocument(options);

        // 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 () {};
        docToShim._ensureMasterEditor = function () {
            if (!this._masterEditor) {
                // Don't let Document create an Editor itself via EditorManager; the unit test can't clean that up
                throw new Error("Use create/destroyMockEditor() to test edit operations");
            }
        };

        return docToShim;
    }

    /**
     * Returns a mock element (in the test runner window) that's offscreen, for
     * parenting UI you want to unit-test. When done, make sure to delete it with
     * remove().
     * @return {jQueryObject} a jQuery object for an offscreen div
     */
    function createMockElement() {
        return $("<div/>")
            .css({
                position: "absolute",
                left: "-10000px",
                top: "-10000px"
            })
            .appendTo($("body"));
    }

    /**
     * Returns an Editor tied to the given Document, but suitable for use in isolation
     * (without being placed inside the surrounding Brackets UI). The Editor *will* be
     * reported as the "active editor" by EditorManager.
     *
     * Must be cleaned up by calling destroyMockEditor(document) later.
     *
     * @return {!Editor}
     */
    function createMockEditorForDocument(doc, visibleRange) {
        // Initialize EditorManager/PanelManager and position the editor-holder offscreen
        // (".content" may not exist, but that's ok for headless tests where editor height doesn't matter)
        var $editorHolder = createMockElement().attr("id", "mock-editor-holder");
        PanelManager._setMockDOM($(".content"), $editorHolder);
        EditorManager.setEditorHolder($editorHolder);

        // create Editor instance
        var editor = new Editor(doc, true, $editorHolder.get(0), visibleRange);
        EditorManager._notifyActiveEditorChanged(editor);

        return editor;
    }

    /**
     * Returns a Document and Editor suitable for use in isolation: the Document
     * will never be set as the currentDocument or added to the working set and the
     * Editor does not live inside a full-blown Brackets UI layout. The Editor *will* be
     * reported as the "active editor" by EditorManager, however.
     *
     * Must be cleaned up by calling destroyMockEditor(document) later.
     *
     * @return {!{doc:!Document, editor:!Editor}}
     */
    function createMockEditor(initialContent, languageId, visibleRange) {
        // create dummy Document, then Editor tied to it
        var doc = createMockDocument(initialContent, languageId);
        return { doc: doc, editor: createMockEditorForDocument(doc, visibleRange) };
    }

    /**
     * Destroy the Editor instance for a given mock Document.
     * @param {!Document} doc  Document whose master editor to destroy
     */
    function destroyMockEditor(doc) {
        EditorManager._notifyActiveEditorChanged(null);
        EditorManager._destroyEditorIfUnneeded(doc);

        // Clear editor holder so EditorManager doesn't try to resize destroyed object
        EditorManager.setEditorHolder(null);
        $("#mock-editor-holder").remove();
    }

    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;

            var params = new UrlParams();

            // setup extension loading in the test window
            params.put("extensions", _doLoadExtensions ?
                        "default,dev," + ExtensionLoader.getUserExtensionPath() :
                        "default");

            // disable update check in test windows
            params.put("skipUpdateCheck", true);

            // disable loading of sample project
            params.put("skipSampleProjectLoad", true);

            // disable initial dialog for live development
            params.put("skipLiveDevelopmentInfo", true);

            _testWindow = window.open(getBracketsSourceRoot() + "/index.html?" + params.toString(), "_blank", optionsStr);

            _testWindow.isBracketsTestWindow = true;

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

            _testWindow.closeAllDocuments = function closeAllDocuments() {
                _testWindow.brackets.test.DocumentManager.closeAll();
            };
        });

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

        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();
            _testWindow.executeCommand = null;
            _testWindow = null;
        });
    }

    /**
     * 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. Also, in cases where asynchronous tasks are performed after the dialog closes,
     * clients must also wait for any additional promises.
     * @param {string} buttonId  One of the Dialogs.DIALOG_BTN_* symbolic constants.
     */
    function clickDialogButton(buttonId) {
        // Make sure there's one and only one dialog open
        var $dlg = _testWindow.$(".modal.instance"),
            promise = $dlg.data("promise");

        expect($dlg.length).toBe(1);

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

        // Click the button
        dismissButton.click();

        // Dialog should resolve/reject the promise
        waitsForDone(promise, "dismiss dialog");
    }

    function loadProjectInTestWindow(path) {
        runs(function () {
            // begin loading project path
            var result = _testWindow.brackets.test.ProjectManager.openProject(path);

            // wait for file system to finish loading
            waitsForDone(result, "ProjectManager.openProject()");
        });
    }

    /**
     * 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,
            charAt  = 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 && exec.index === i);

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

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

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

                if (charAt === '\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();
        }).always(function () {
            docs = null;
            FileViewController = null;
        });

        return result.promise();
    }

    /**
     * Create or overwrite a text file
     * @param {!string} path Path for a file to be created/overwritten
     * @param {!string} text Text content for the new file
     * @return {$.Promise} A promise resolved when the file is written or rejected when an error occurs.
     */
    function createTextFile(path, text) {
        var deferred = new $.Deferred();

        getRoot().done(function (nfs) {
            // create the new FileEntry
            nfs.getFile(path, { create: true }, function success(entry) {
                // write text this new FileEntry
                FileUtils.writeText(entry, text).done(function () {
                    deferred.resolve(entry);
                }).fail(function () {
                    deferred.reject();
                });
            }, function error() {
                deferred.reject();
            });
        });

        return deferred.promise();
    }

    /**
     * Copy a file source path to a destination
     * @param {!FileEntry} source Entry for the source file to copy
     * @param {!string} destination Destination path to copy the source file
     * @param {?{parseOffsets:boolean}} options parseOffsets allows optional
     *     offset markup parsing. File is written to the destination path
     *     without offsets. Offset data is passed to the doneCallbacks of the
     *     promise.
     * @return {$.Promise} A promise resolved when the file is copied to the
     *     destination.
     */
    function copyFileEntry(source, destination, options) {
        options = options || {};

        var deferred = new $.Deferred();

        // read the source file
        FileUtils.readAsText(source).done(function (text, modificationTime) {
            getRoot().done(function (nfs) {
                var offsets;

                // optionally parse offsets
                if (options.parseOffsets) {
                    var parseInfo = parseOffsetsFromText(text);
                    text = parseInfo.text;
                    offsets = parseInfo.offsets;
                }

                // create the new FileEntry
                createTextFile(destination, text).done(function (entry) {
                    deferred.resolve(entry, offsets, text);
                }).fail(function () {
                    deferred.reject();
                });
            });
        }).fail(function () {
            deferred.reject();
        });

        return deferred.promise();
    }

    /**
     * Copy a directory source to a destination
     * @param {!DirectoryEntry} source Entry for the source directory to copy
     * @param {!string} destination Destination path to copy the source directory
     * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options
     *     parseOffsets - allows optional offset markup parsing. File is written to the
     *       destination path without offsets. Offset data is passed to the
     *       doneCallbacks of the promise.
     *     infos - an optional Object used when parseOffsets is true. Offset
     *       information is attached here, indexed by the file destination path.
     *     removePrefix - When parseOffsets is true, set removePrefix true
     *       to add a new key to the infos array that drops the destination
     *       path root.
     * @return {$.Promise} A promise resolved when the directory and all it's
     *     contents are copied to the destination or rejected immediately
     *     upon the first error.
     */
    function copyDirectoryEntry(source, destination, options) {
        options = options || {};
        options.infos = options.infos || {};

        var parseOffsets    = options.parseOffsets || false,
            removePrefix    = options.removePrefix || true,
            deferred        = new $.Deferred();

        // create the destination folder
        brackets.fs.makedir(destination, parseInt("644", 8), function callback(err) {
            if (err && err !== brackets.fs.ERR_FILE_EXISTS) {
                deferred.reject();
                return;
            }

            source.createReader().readEntries(function handleEntries(entries) {
                if (entries.length === 0) {
                    deferred.resolve();
                    return;
                }

                // copy all children of this directory
                var copyChildrenPromise = Async.doInParallel(
                    entries,
                    function copyChild(child) {
                        var childDestination = destination + "/" + child.name,
                            promise;

                        if (child.isDirectory) {
                            promise = copyDirectoryEntry(child, childDestination, options);
                        } else {
                            promise = copyFileEntry(child, childDestination, options);

                            if (parseOffsets) {
                                // save offset data for each file path
                                promise.done(function (destinationEntry, offsets, text) {
                                    options.infos[childDestination] = {
                                        offsets     : offsets,
                                        fileEntry   : destinationEntry,
                                        text        : text
                                    };
                                });
                            }
                        }

                        return promise;
                    },
                    true
                );

                copyChildrenPromise.then(deferred.resolve, deferred.reject);
            });
        });

        deferred.always(function () {
            // remove destination path prefix
            if (removePrefix && options.infos) {
                var shortKey;
                Object.keys(options.infos).forEach(function (key) {
                    shortKey = key.substr(destination.length + 1);
                    options.infos[shortKey] = options.infos[key];
                });
            }
        });

        return deferred.promise();
    }

    /**
     * Copy a file or directory source path to a destination
     * @param {!string} source Path for the source file or directory to copy
     * @param {!string} destination Destination path to copy the source file or directory
     * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options
     *     parseOffsets - allows optional offset markup parsing. File is written to the
     *       destination path without offsets. Offset data is passed to the
     *       doneCallbacks of the promise.
     *     infos - an optional Object used when parseOffsets is true. Offset
     *       information is attached here, indexed by the file destination path.
     *     removePrefix - When parseOffsets is true, set removePrefix true
     *       to add a new key to the infos array that drops the destination
     *       path root.
     * @return {$.Promise} A promise resolved when the directory and all it's
     *     contents are copied to the destination or rejected immediately
     *     upon the first error.
     */
    function copyPath(source, destination, options) {
        var deferred = new $.Deferred();

        resolveNativeFileSystemPath(source).done(function (entry) {
            var promise;

            if (entry.isDirectory) {
                promise = copyDirectoryEntry(entry, destination, options);
            } else {
                promise = copyFileEntry(entry, destination, options);
            }

            promise.then(deferred.resolve, deferred.reject);
        }).fail(function () {
            deferred.reject();
        });

        return deferred.promise();
    }

    /**
     * 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 toggleQuickEditAtOffset(editor, offset) {
        editor.setCursorPos(offset.line, offset.ch);

        return _testWindow.executeCommand(Commands.TOGGLE_QUICK_EDIT);
    }

    /**
     * @param {string} fullPath
     * @return {$.Promise} Resolved when deletion complete, or rejected if an error occurs
     */
    function deletePath(fullPath) {
        var result = new $.Deferred();
        brackets.fs.unlink(fullPath, function (err) {
            if (err) {
                console.error(err);
                result.reject(err);
            } else {
                result.resolve();
            }
        });

        return result.promise();
    }

    /**
     * Simulate key event. Found this code here:
     * http://stackoverflow.com/questions/10455626/keydown-simulation-in-chrome-fires-normally-but-not-the-correct-key
     *
     * TODO: need parameter(s) for modifier keys
     *
     * @param {Number} key Key code
     * @param (String) event Key event to simulate
     * @param {HTMLElement} element Element to receive event
     */
    function simulateKeyEvent(key, event, element) {
        var doc = element.ownerDocument,
            oEvent = doc.createEvent('KeyboardEvent');

        if (event !== "keydown" && event !== "keyup" && event !== "keypress") {
            console.log("SpecRunnerUtils.simulateKeyEvent() - unsupported keyevent: " + event);
            return;
        }

        // Chromium Hack: need to override the 'which' property.
        // Note: this code is not designed to work in IE, Safari,
        // or other browsers. Well, maybe with Firefox. YMMV.
        Object.defineProperty(oEvent, 'keyCode', {
            get: function () {
                return this.keyCodeVal;
            }
        });
        Object.defineProperty(oEvent, 'which', {
            get: function () {
                return this.keyCodeVal;
            }
        });
        Object.defineProperty(oEvent, 'charCode', {
            get: function () {
                return this.keyCodeVal;
            }
        });

        if (oEvent.initKeyboardEvent) {
            oEvent.initKeyboardEvent(event, true, true, doc.defaultView, key, 0, false, false, false, false);
        } else {
            oEvent.initKeyEvent(event, true, true, doc.defaultView, false, false, false, false, key, 0);
        }

        oEvent.keyCodeVal = key;
        if (oEvent.keyCode !== key) {
            console.log("keyCode mismatch " + oEvent.keyCode + "(" + oEvent.which + ")");
        }

        element.dispatchEvent(oEvent);
    }

    function getTestWindow() {
        return _testWindow;
    }

    function setLoadExtensionsInTestWindow(doLoadExtensions) {
        _doLoadExtensions = doLoadExtensions;
    }

    /**
     * Extracts the jasmine.log() and/or jasmine.expect() messages from the given result,
     * including stack traces if available.
     * @param {Object} result A jasmine result item (from results.getItems()).
     * @return {string} the error message from that item.
     */
    function getResultMessage(result) {
        var message;
        if (result.type === 'log') {
            message = result.toString();
        } else if (result.type === 'expect' && result.passed && !result.passed()) {
            message = result.message;

            if (result.trace.stack) {
                message = result.trace.stack;
            }
        }
        return message;
    }

    /**
     * Set permissions on a path
     * @param {!string} path Path to change permissions on
     * @param {!string} mode New mode as an octal string
     * @return {$.Promise} Resolved when permissions are set or rejected if an error occurs
     */
    function chmod(path, mode) {
        var deferred = new $.Deferred();

        brackets.fs.chmod(path, parseInt(mode, 8), function (err) {
            if (err) {
                deferred.reject(err);
            } else {
                deferred.resolve();
            }
        });

        return deferred.promise();
    }

    /**
     * Remove a directory (recursively) or file
     *
     * @param {!string} path Path to remove
     * @return {$.Promise} Resolved when the path is removed, rejected if there was a problem
     */
    function remove(path) {
        var d = new $.Deferred();
        var nodeDeferred = brackets.testing.getNodeConnectionDeferred();
        nodeDeferred
            .done(function (connection) {
                if (connection.connected()) {
                    connection.domains.testing.remove(path)
                        .done(function () {
                            d.resolve();
                        })
                        .fail(function () {
                            d.reject();
                        });
                } else {
                    d.reject();
                }
            })
            .fail(function () {
                d.reject();
            });
        return d.promise();
    }

    /**
     * Searches the DOM tree for text containing the given content. Useful for verifying
     * that data you expect to show up in the UI somewhere is actually there.
     *
     * @param {jQueryObject|Node} root The root element to search from. Can be either a jQuery object
     *     or a raw DOM node.
     * @param {string} content The content to find.
     * @param {boolean} asLink If true, find the content in the href of an <a> tag, otherwise find it in text nodes.
     * @return true if content was found
     */
    function findDOMText(root, content, asLink) {
        // Unfortunately, we can't just use jQuery's :contains() selector, because it appears that
        // you can't escape quotes in it.
        var i;
        if (root instanceof $) {
            root = root.get(0);
        }
        if (!root) {
            return false;
        } else if (!asLink && root.nodeType === 3) { // text node
            return root.textContent.indexOf(content) !== -1;
        } else {
            if (asLink && root.nodeType === 1 && root.tagName.toLowerCase() === "a" && root.getAttribute("href") === content) {
                return true;
            }
            var children = root.childNodes;
            for (i = 0; i < children.length; i++) {
                if (findDOMText(children[i], content, asLink)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Counts the number of active specs in the current suite. Includes all
     * descendants.
     * @param {(jasmine.Suite|jasmine.Spec)} suiteOrSpec
     * @return {number}
     */
    function countSpecs(suiteOrSpec) {
        var children = suiteOrSpec.children && typeof suiteOrSpec.children === "function" && suiteOrSpec.children();

        if (Array.isArray(children)) {
            var childCount = 0;

            children.forEach(function (child) {
                childCount += countSpecs(child);
            });

            return childCount;
        }

        if (jasmine.getEnv().specFilter(suiteOrSpec)) {
            return 1;
        }

        return 0;
    }

    beforeEach(function () {
        this.addMatchers({
            /**
             * Expects the given editor's selection to be a cursor at the given position (no range selected)
             */
            toHaveCursorPosition: function (line, ch) {
                var editor = this.actual;
                var selection = editor.getSelection();
                var notString = this.isNot ? "not " : "";

                var start = selection.start;
                var end = selection.end;
                var selectionMoreThanOneCharacter = start.line !== end.line || start.ch !== end.ch;

                this.message = function () {
                    var message = "Expected the cursor to " + notString + "be at (" + line + ", " + ch +
                        ") but it was actually at (" + start.line + ", " + start.ch + ")";
                    if (!this.isNot && selectionMoreThanOneCharacter) {
                        message += " and more than one character was selected.";
                    }
                    return message;
                };

                var positionsMatch = start.line === line && start.ch === ch;

                // when adding the not operator, it's confusing to check both the size of the
                // selection and the position. We just check the position in that case.
                if (this.isNot) {
                    return positionsMatch;
                } else {
                    return !selectionMoreThanOneCharacter && positionsMatch;
                }
            }
        });
    });

    exports.TEST_PREFERENCES_KEY    = TEST_PREFERENCES_KEY;

    exports.chmod                           = chmod;
    exports.remove                          = remove;
    exports.getTestRoot                     = getTestRoot;
    exports.getTestPath                     = getTestPath;
    exports.getTempDirectory                = getTempDirectory;
    exports.createTempDirectory             = createTempDirectory;
    exports.getBracketsSourceRoot           = getBracketsSourceRoot;
    exports.makeAbsolute                    = makeAbsolute;
    exports.resolveNativeFileSystemPath     = resolveNativeFileSystemPath;
    exports.createMockDocument              = createMockDocument;
    exports.createMockActiveDocument        = createMockActiveDocument;
    exports.createMockElement               = createMockElement;
    exports.createMockEditorForDocument     = createMockEditorForDocument;
    exports.createMockEditor                = createMockEditor;
    exports.createTestWindowAndRun          = createTestWindowAndRun;
    exports.closeTestWindow                 = closeTestWindow;
    exports.clickDialogButton               = clickDialogButton;
    exports.destroyMockEditor               = destroyMockEditor;
    exports.loadProjectInTestWindow         = loadProjectInTestWindow;
    exports.openProjectFiles                = openProjectFiles;
    exports.toggleQuickEditAtOffset         = toggleQuickEditAtOffset;
    exports.createTextFile                  = createTextFile;
    exports.copyDirectoryEntry              = copyDirectoryEntry;
    exports.copyFileEntry                   = copyFileEntry;
    exports.copyPath                        = copyPath;
    exports.deletePath                      = deletePath;
    exports.getTestWindow                   = getTestWindow;
    exports.simulateKeyEvent                = simulateKeyEvent;
    exports.setLoadExtensionsInTestWindow   = setLoadExtensionsInTestWindow;
    exports.getResultMessage                = getResultMessage;
    exports.parseOffsetsFromText            = parseOffsetsFromText;
    exports.findDOMText                     = findDOMText;
    exports.countSpecs                      = countSpecs;
});


/*!
 * Connect - HTTPServer
 * Copyright(c) 2010 Sencha Inc.
 * Copyright(c) 2011 TJ Holowaychuk
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var http = require('http')
  , utils = require('./utils')
  , debug = require('debug')('connect:dispatcher');

// prototype

var app = module.exports = {};

// environment

var env = process.env.NODE_ENV || 'development';

/**
 * Utilize the given middleware `handle` to the given `route`,
 * defaulting to _/_. This "route" is the mount-point for the
 * middleware, when given a value other than _/_ the middleware
 * is only effective when that segment is present in the request's
 * pathname.
 *
 * For example if we were to mount a function at _/admin_, it would
 * be invoked on _/admin_, and _/admin/settings_, however it would
 * not be invoked for _/_, or _/posts_.
 *
 * Examples:
 *
 *      var app = connect();
 *      app.use(connect.favicon());
 *      app.use(connect.logger());
 *      app.use(connect.static(__dirname + '/public'));
 *
 * If we wanted to prefix static files with _/public_, we could
 * "mount" the `static()` middleware:
 *
 *      app.use('/public', connect.static(__dirname + '/public'));
 *
 * This api is chainable, so the following is valid:
 *
 *      connect()
 *        .use(connect.favicon())
 *        .use(connect.logger())
 *        .use(connect.static(__dirname + '/public'))
 *        .listen(3000);
 *
 * @param {String|Function|Server} route, callback or server
 * @param {Function|Server} callback or server
 * @return {Server} for chaining
 * @api public
 */

app.use = function(route, fn){
  // default route to '/'
  if ('string' != typeof route) {
    fn = route;
    route = '/';
  }

  // wrap sub-apps
  if ('function' == typeof fn.handle) {
    var server = fn;
    fn.route = route;
    fn = function(req, res, next){
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  if (fn instanceof http.Server) {
    fn = fn.listeners('request')[0];
  }

  // strip trailing slash
  if ('/' == route[route.length - 1]) {
    route = route.slice(0, -1);
  }

  // add the middleware
  debug('use %s %s', route || '/', fn.name || 'anonymous');
  this.stack.push({ route: route, handle: fn });

  return this;
};

/**
 * Handle server requests, punting them down
 * the middleware stack.
 *
 * @api private
 */

app.handle = function(req, res, out) {
  var stack = this.stack
    , fqdn = ~req.url.indexOf('://')
    , removed = ''
    , slashAdded = false
    , index = 0;

  function next(err) {
    var layer, path, status, c;

    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    req.url = removed + req.url;
    req.originalUrl = req.originalUrl || req.url;
    removed = '';

    // next callback
    layer = stack[index++];

    // all done
    if (!layer || res.headerSent) {
      // delegate to parent
      if (out) return out(err);

      // unhandled error
      if (err) {
        // default to 500
        if (res.statusCode < 400) res.statusCode = 500;
        debug('default %s', res.statusCode);

        // respect err.status
        if (err.status) res.statusCode = err.status;

        // production gets a basic error message
        var msg = 'production' == env
          ? http.STATUS_CODES[res.statusCode]
          : err.stack || err.toString();

        // log to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());
        if (res.headerSent) return req.socket.destroy();
        res.setHeader('Content-Type', 'text/plain');
        res.setHeader('Content-Length', Buffer.byteLength(msg));
        if ('HEAD' == req.method) return res.end();
        res.end(msg);
      } else {
        debug('default 404');
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl));
      }
      return;
    }

    try {
      path = utils.parseUrl(req).pathname;
      if (undefined == path) path = '/';

      // skip this layer if the route doesn't match.
      if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err);

      c = path[layer.route.length];
      if (c && '/' != c && '.' != c) return next(err);

      // Call the layer handler
      // Trim off the part of the url that matches the route
      removed = layer.route;
      req.url = req.url.substr(removed.length);

      // Ensure leading slash
      if (!fqdn && '/' != req.url[0]) {
        req.url = '/' + req.url;
        slashAdded = true;
      }

      debug('%s', layer.handle.name || 'anonymous');
      var arity = layer.handle.length;
      if (err) {
        if (arity === 4) {
          layer.handle(err, req, res, next);
        } else {
          next(err);
        }
      } else if (arity < 4) {
        layer.handle(req, res, next);
      } else {
        next();
      }
    } catch (e) {
      next(e);
    }
  }
  next();
};

/**
 * Listen for connections.
 *
 * This method takes the same arguments
 * as node's `http.Server#listen()`.
 *
 * HTTP and HTTPS:
 *
 * If you run your application both as HTTP
 * and HTTPS you may wrap them individually,
 * since your Connect "server" is really just
 * a JavaScript `Function`.
 *
 *      var connect = require('connect')
 *        , http = require('http')
 *        , https = require('https');
 *
 *      var app = connect();
 *
 *      http.createServer(app).listen(80);
 *      https.createServer(options, app).listen(443);
 *
 * @return {http.Server}
 * @api public
 */

app.listen = function(){
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};


var Sntp = require('../lib');

// All options are optional

var options = {
    host: 'nist1-sj.ustiming.org',  // Defaults to pool.ntp.org
    port: 123,                      // Defaults to 123 (NTP)
    resolveReference: true,         // Default to false (not resolving)
    timeout: 1000                   // Defaults to zero (no timeout)
};

// Request server time

Sntp.time(options, function (err, time) {

    if (err) {
        console.log('Failed: ' + err.message);
        process.exit(1);
    }

    console.log(time);
    console.log('Local clock is off by: ' + time.t + ' milliseconds');
    process.exit(0);
});


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Slider - Multiple sliders</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

	Master volume

	Graphic EQ

	88
	77
	55
	33
	40
	45
	70

Combine horizontal and vertical sliders, each with their own options, to create the UI for a music player.


keep keep keep replace keep keep keep keep keep

var promise2 = FileIndexManager.getFileInfoList("css") // different request in parallel


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Draggable - Revert position</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Revert the original

Revert the helper

Return the draggable (or it's helper) to its original location when dragging stops with the boolean revert option.


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Dialog - Modal form</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

All form fields are required.

	<form>
	<fieldset>
		Name
		<input type="text" name="name" id="name" class="text ui-widget-content ui-corner-all" />
		Email
		<input type="text" name="email" id="email" value="" class="text ui-widget-content ui-corner-all" />
		Password
		<input type="password" name="password" id="password" value="" class="text ui-widget-content ui-corner-all" />
	</fieldset>
	</form>

Existing Users:

				Name
				Email
				Password

				John Doe
				john.doe@example.com
				johndoe1

<button id="create-user">Create new user</button>

Use a modal dialog to require that the user enter data during a multi-step process.  Embed form markup in the content area, set the modal option to true, and specify primary and secondary user actions with the buttons option.


/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */
/* Written by Mathias Bynens <http://mathiasbynens.be/> */
jQuery(function($){
	$.datepicker.regional.nl = {
		closeText: 'Sluiten',
		prevText: '←',
		nextText: '→',
		currentText: 'Vandaag',
		monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',
		'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
		monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun',
		'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
		dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
		dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],
		dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
		weekHeader: 'Wk',
		dateFormat: 'dd-mm-yy',
		firstDay: 1,
		isRTL: false,
		showMonthAfterYear: false,
		yearSuffix: ''};
	$.datepicker.setDefaults($.datepicker.regional.nl);
});