Modules (188)

LiveDevelopment

Description

LiveDevelopment manages the Inspector, all Agents, and the active LiveDocument

STARTING

To start a session call open. This will read the currentDocument from brackets, launch the LiveBrowser (currently Chrome) with the remote debugger port open, establish the Inspector connection to the remote debugger, and finally load all agents.

STOPPING

To stop a session call close. This will close the active browser window, disconnect the Inspector, unload all agents, and clean up.

STATUS

Status updates are dispatched as statusChange jQuery events. The status is passed as the first parameter and the reason for the change as the second parameter. Currently only the "Inactive" status supports the reason parameter. The status codes are:

-1: Error
 0: Inactive
 1: Connecting to the remote debugger
 2: Loading agents
 3: Active
 4: Out of sync
 5: Sync error

The reason codes are:

  • null (Unknown reason)
  • "explicit_close" (LiveDevelopment.close() was called)
  • "navigated_away" (The browser changed to a location outside of the project)
  • "detached_target_closed" (The tab or window was closed)
  • "detached_replaced_with_devtools" (The developer tools were opened in the browser)

Dependencies

Variables

Private

_closeDeferred

Promise returned for each call to close()

Type
jQuery.Deferred
    var _closeDeferred;

    // Disallow re-entrancy of loadAgents()
    var _loadAgentsPromise;
Private

_liveDocument

Live Preview current Document info

Type
HTMLDocument
    var _liveDocument;
Private

_loadedAgentNames

Store the names (matching property names in the 'agent' object) of agents that we've loaded

Type
string
    var _loadedAgentNames = [];
Private

_openDeferred

Promise returned for each call to open()

Type
jQuery.Deferred
    var _openDeferred;
Private

_regServers

    var _regServers = [];
    
    PreferencesManager.definePreference("livedev.wsPort", "number", 8125, {
        description: Strings.DESCRIPTION_LIVEDEV_WEBSOCKET_PORT
    });
    
    PreferencesManager.definePreference("livedev.enableReverseInspect", "boolean", true, {
        description: Strings.DESCRIPTION_LIVEDEV_ENABLE_REVERSE_INSPECT
    });

    function _isPromisePending(promise) {
        return promise && promise.state() === "pending";
    }
Private

_relatedDocuments

Related Live Documents

Type
Object.<string: (HTMLDocument, CSSDocument)>
    var _relatedDocuments = {};
Private

_server

Current live preview server

Type
BaseServer
    var _server;

Functions

Private

_classForDocument

Determine which document class should be used for a given document

document Document
    function _classForDocument(doc) {
        switch (doc.getLanguage().getId()) {
        case "less":
        case "scss":
            return CSSPreprocessorDocument;
        case "css":
            return CSSDocument;
        case "javascript":
            return exports.config.experimental ? JSDocument : null;
        }

        if (LiveDevelopmentUtils.isHtmlFileExt(doc.file.fullPath)) {
            return HTMLDocument;
        }

        return null;
    }

    function getLiveDocForPath(path) {
        if (!_server) {
            return undefined;
        }

        return _server.get(path);
    }

    function getLiveDocForEditor(editor) {
        if (!editor) {
            return null;
        }
        return getLiveDocForPath(editor.document.file.fullPath);
    }
Private

_close

doCloseWindow boolean
Use true to close the window/tab in the browser
reason nullable string
Optional string key suffix to display to user (see LIVE_DEV_* keys)
Returns: jQuery.Promise
Always return a resolved promise once the connection is closed
    function _close(doCloseWindow, reason) {
        WebSocketTransport.closeWebSocketServer();
        if (_closeDeferred) {
            return _closeDeferred;
        } else {
            _closeDeferred = new $.Deferred();
            _closeDeferred.always(function () {
                _closeDeferred = null;
            });
        }

        var promise = _closeDeferred.promise();
Private

_closeDocument

    function _closeDocument(liveDocument) {
        _doClearErrors(liveDocument);
        liveDocument.close();

        if (liveDocument.editor) {
            liveDocument.editor.off(".livedev");
        }

        liveDocument.off(".livedev");
    }
Private

_closeDocuments

    function _closeDocuments() {
        if (_liveDocument) {
            _closeDocument(_liveDocument);
            _liveDocument = undefined;
        }

        Object.keys(_relatedDocuments).forEach(function (url) {
            _closeDocument(_relatedDocuments[url]);
            delete _relatedDocuments[url];
        });

        // Clear all documents from request filtering
        if (_server) {
            _server.clear();
        }
    }
Private

_closeRelatedDocument

Removes the given CSS/JSDocument from _relatedDocuments. Signals that the given file is no longer associated with the HTML document that is live (e.g. if the related file has been deleted on disk).

    function _closeRelatedDocument(liveDoc) {
        if (_relatedDocuments[liveDoc.doc.url]) {
            delete _relatedDocuments[liveDoc.doc.url];
        }

        if (_server) {
            _server.remove(liveDoc);
        }

        _closeDocument(liveDoc);
    }
Private

_createDocument

doc Document
Current document
editor Editor
Current editor
Returns: ?(HTMLDocument,CSSDocument)
    function _createDocument(doc, editor) {
        var DocClass        = _classForDocument(doc),
            liveDocument    = new DocClass(doc, editor);

        if (!DocClass) {
            return null;
        }

        liveDocument.on("statusChanged.livedev", function () {
            _handleLiveDocumentStatusChanged(liveDocument);
        });

        return liveDocument;
    }
Private

_createLiveDocumentForFrame

doc Document
Current document
    function _createLiveDocumentForFrame(doc) {
        // create live document
        doc._ensureMasterEditor();
        _liveDocument = _createDocument(doc, doc._masterEditor);
        _server.add(_liveDocument);
    }
Private

_doClearErrors

liveDocument HTMLDocument,CSSDocument
    function _doClearErrors(liveDocument) {
        var lineHandle;

        if (!liveDocument.editor ||
                !liveDocument._errorLineHandles ||
                !liveDocument._errorLineHandles.length) {
            return;
        }

        liveDocument.editor._codeMirror.operation(function () {
            while (true) {
                // Iterate over all lines that were previously marked with an error
                lineHandle = liveDocument._errorLineHandles.pop();

                if (!lineHandle) {
                    break;
                }

                liveDocument.editor._codeMirror.removeLineClass(lineHandle, "wrap", SYNC_ERROR_CLASS);
            }
        });
    }
Private

_doInspectorDisconnect

doCloseWindow boolean
Use true to close the window/tab in the browser
Returns: jQuery.Promise
A promise that is always resolved
    function _doInspectorDisconnect(doCloseWindow) {
        var closePromise,
            deferred    = new $.Deferred(),
            connected   = Inspector.connected();

        EditorManager.off("activeEditorChange", onActiveEditorChange);

        Inspector.Page.off(".livedev");
        Inspector.off(".livedev");

        // Wait if agents are loading
        if (_loadAgentsPromise) {
            _loadAgentsPromise.always(unloadAgents);
        } else {
            unloadAgents();
        }

        // Close live documents
        _closeDocuments();

        if (_server) {
            // Stop listening for requests when disconnected
            _server.stop();

            // Dispose server
            _server = null;
        }

        if (doCloseWindow && connected) {
            closePromise = Inspector.Runtime.evaluate("window.open('', '_self').close();");

            // Add a timeout to continue cleanup if Inspector does not respond
            closePromise = Async.withTimeout(closePromise, 5000);
        } else {
            closePromise = new $.Deferred().resolve();
        }

        // Disconnect WebSocket if connected
        closePromise.always(function () {
            if (Inspector.connected()) {
                Inspector.disconnect().always(deferred.resolve);
            } else {
                deferred.resolve();
            }
        });

        return deferred.promise();
    }
Private

_docIsOutOfSync

Documents are considered to be out-of-sync if they are dirty and do not have "update while editing" support

doc Document
    function _docIsOutOfSync(doc) {
        var liveDoc = _server && _server.get(doc.file.fullPath),
            isLiveEditingEnabled = liveDoc && liveDoc.isLiveEditingEnabled();

        return doc.isDirty && !isLiveEditingEnabled;
    }
Private

_enableAgents

    function _enableAgents() {
        // enable agents in parallel
        return Async.doInParallel(
            getEnabledAgents(),
            function (name) {
                return _invokeAgentMethod(name, "enable");
            },
            true
        );
    }
Private

_getCurrentDocument

Get the current document from the document manager _adds extension, url and root to the document

    function _getCurrentDocument() {
        return DocumentManager.getCurrentDocument();
    }
Private Public API

_getInitialDocFromCurrent

Returns: jQuery.Promise
A promise that is resolved with a full path to a file if one could been determined, or null if there was no suitable index file.
    function _getInitialDocFromCurrent() {
        var doc = _getCurrentDocument(),
            refPath,
            i;

        // Is the currently opened document already a file we can use for Live Development?
        if (doc) {
            refPath = doc.file.fullPath;
            if (LiveDevelopmentUtils.isStaticHtmlFileExt(refPath) || LiveDevelopmentUtils.isServerHtmlFileExt(refPath)) {
                return new $.Deferred().resolve(doc);
            }
        }

        var result = new $.Deferred();

        var baseUrl = ProjectManager.getBaseUrl(),
            hasOwnServerForLiveDevelopment = (baseUrl && baseUrl.length);

        ProjectManager.getAllFiles().done(function (allFiles) {
            var projectRoot = ProjectManager.getProjectRoot().fullPath,
                containingFolder,
                indexFileFound = false,
                stillInProjectTree = true;

            if (refPath) {
                containingFolder = FileUtils.getDirectoryPath(refPath);
            } else {
                containingFolder = projectRoot;
            }

            var filteredFiltered = allFiles.filter(function (item) {
                var parent = FileUtils.getParentPath(item.fullPath);

                return (containingFolder.indexOf(parent) === 0);
            });

            var filterIndexFile = function (fileInfo) {
                if (fileInfo.fullPath.indexOf(containingFolder) === 0) {
                    if (FileUtils.getFilenameWithoutExtension(fileInfo.name) === "index") {
                        if (hasOwnServerForLiveDevelopment) {
                            if ((LiveDevelopmentUtils.isServerHtmlFileExt(fileInfo.name)) ||
                                    (LiveDevelopmentUtils.isStaticHtmlFileExt(fileInfo.name))) {
                                return true;
                            }
                        } else if (LiveDevelopmentUtils.isStaticHtmlFileExt(fileInfo.name)) {
                            return true;
                        }
                    } else {
                        return false;
                    }
                }
            };

            while (!indexFileFound && stillInProjectTree) {
                i = _.findIndex(filteredFiltered, filterIndexFile);

                // We found no good match
                if (i === -1) {
                    // traverse the directory tree up one level
                    containingFolder = FileUtils.getParentPath(containingFolder);
                    // Are we still inside the project?
                    if (containingFolder.indexOf(projectRoot) === -1) {
                        stillInProjectTree = false;
                    }
                } else {
                    indexFileFound = true;
                }
            }

            if (i !== -1) {
                DocumentManager.getDocumentForPath(filteredFiltered[i].fullPath).then(result.resolve, result.resolve);
                return;
            }

            result.resolve(null);
        });

        return result.promise();
    }
Private

_handleLiveDocumentStatusChanged

event $.Event
liveDocument HTMLDocument,CSSDocument
errors Array.<{token: SimpleNode,startPos: Pos,endPos: Pos}>
    function _handleLiveDocumentStatusChanged(liveDocument) {
        var startLine,
            endLine,
            i,
            lineHandle,
            status = (liveDocument.errors.length) ? STATUS_SYNC_ERROR : STATUS_ACTIVE;

        _setStatus(status);

        if (!liveDocument.editor) {
            return;
        }

        // Buffer addLineClass DOM changes in a CodeMirror operation
        liveDocument.editor._codeMirror.operation(function () {
            // Remove existing errors before marking new ones
            _doClearErrors(liveDocument);

            liveDocument._errorLineHandles = liveDocument._errorLineHandles || [];

            liveDocument.errors.forEach(function (error) {
                startLine = error.startPos.line;
                endLine = error.endPos.line;

                for (i = startLine; i < endLine + 1; i++) {
                    lineHandle = liveDocument.editor._codeMirror.addLineClass(i, "wrap", SYNC_ERROR_CLASS);
                    liveDocument._errorLineHandles.push(lineHandle);
                }
            });
        });
    }
Private

_invokeAgentMethod

name string
Agent name
methodName stirng
Method name to call on the agent
    function _invokeAgentMethod(name, methodName) {
        var oneAgentPromise;

        if (agents[name] && agents[name][methodName]) {
            oneAgentPromise = agents[name][methodName].call();
        }

        if (!oneAgentPromise) {
            oneAgentPromise = new $.Deferred().resolve().promise();
        } else {
            oneAgentPromise.fail(function () {
                console.error(methodName + " failed on agent", name);
            });
        }

        return oneAgentPromise;
    }

    function getEnabledAgents() {
        var enabledAgents;

        // Select agents to use
        if (exports.config.experimental) {
            // load all agents
            enabledAgents = agents;
        } else {
            // load only enabled agents
            enabledAgents = _enabledAgentNames;
        }

        return Object.keys(enabledAgents);
    }
Private

_makeTroubleshootingMessage

msg string
Original message
Returns: string
Original message plus link to troubleshooting page.
    function _makeTroubleshootingMessage(msg) {
        return msg + " " + StringUtils.format(Strings.LIVE_DEVELOPMENT_TROUBLESHOOTING, brackets.config.troubleshoot_url);
    }
Private

_onConnect

Triggered by Inspector.connect

    function _onConnect(event) {
        // When the browser navigates away from the primary live document
        Inspector.Page.on("frameNavigated.livedev", _onFrameNavigated);

        // When the Inspector WebSocket disconnects unexpectedely
        Inspector.on("disconnect.livedev", _onDisconnect);

        _waitForInterstitialPageLoad()
            .fail(function () {
                close();

                Dialogs.showModalDialog(
                    DefaultDialogs.DIALOG_ID_ERROR,
                    Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
                    _makeTroubleshootingMessage(Strings.LIVE_DEV_LOADING_ERROR_MESSAGE)
                );
            })
            .done(_onInterstitialPageLoad);
    }

    function _showWrongDocError() {
        Dialogs.showModalDialog(
            DefaultDialogs.DIALOG_ID_ERROR,
            Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
            _makeTroubleshootingMessage(Strings.LIVE_DEV_NEED_HTML_MESSAGE)
        );
        _openDeferred.reject();
    }

    function _showLiveDevServerNotReadyError() {
        Dialogs.showModalDialog(
            DefaultDialogs.DIALOG_ID_ERROR,
            Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
            _makeTroubleshootingMessage(Strings.LIVE_DEV_SERVER_NOT_READY_MESSAGE)
        );
        _openDeferred.reject();
    }

    function _openInterstitialPage() {
        var browserStarted  = false,
            retryCount      = 0;

        // Open the live browser if the connection fails, retry 3 times
        Inspector.connectToURL(launcherUrl).fail(function onConnectFail(err) {
            if (err === "CANCEL") {
                _openDeferred.reject(err);
                return;
            }

            if (retryCount > 3) {
                _setStatus(STATUS_ERROR);

                var dialogPromise = Dialogs.showModalDialog(
                    DefaultDialogs.DIALOG_ID_LIVE_DEVELOPMENT,
                    Strings.LIVE_DEVELOPMENT_RELAUNCH_TITLE,
                    _makeTroubleshootingMessage(Strings.LIVE_DEVELOPMENT_ERROR_MESSAGE),
                    [
                        {
                            className: Dialogs.DIALOG_BTN_CLASS_LEFT,
                            id:        Dialogs.DIALOG_BTN_CANCEL,
                            text:      Strings.CANCEL
                        },
                        {
                            className: Dialogs.DIALOG_BTN_CLASS_PRIMARY,
                            id:        Dialogs.DIALOG_BTN_OK,
                            text:      Strings.RELAUNCH_CHROME
                        }
                    ]
                );

                dialogPromise.done(function (id) {
                    if (id === Dialogs.DIALOG_BTN_OK) {
                        // User has chosen to reload Chrome, quit the running instance
                        _setStatus(STATUS_INACTIVE);
                        _close()
                            .done(function () {
                                browserStarted = false;
                                // Continue to use _openDeferred
                                open(true);
                            })
                            .fail(function (err) {
                                // Report error?
                                _setStatus(STATUS_ERROR);
                                browserStarted = false;
                                _openDeferred.reject("CLOSE_LIVE_BROWSER");
                            });
                    } else {
                        _close()
                            .done(function () {
                                browserStarted = false;
                                _openDeferred.reject("CANCEL");
                            })
                            .fail(function (err) {
                                // Report error?
                                _setStatus(STATUS_ERROR);
                                browserStarted = false;
                                _openDeferred.reject("CLOSE_LIVE_BROWSER");
                            });
                    }
                });

                return;
            }
            retryCount++;

            if (!browserStarted && exports.status !== STATUS_ERROR) {
                NativeApp.openLiveBrowser(
                    launcherUrl,
                    true        // enable remote debugging
                )
                    .done(function () {
                        browserStarted = true;
                    })
                    .fail(function (err) {
                        var message;

                        _setStatus(STATUS_ERROR);
                        if (err === FileSystemError.NOT_FOUND) {
                            message = Strings.ERROR_CANT_FIND_CHROME;
                        } else {
                            message = StringUtils.format(Strings.ERROR_LAUNCHING_BROWSER, err);
                        }

                        Dialogs.showModalDialog(
                            DefaultDialogs.DIALOG_ID_ERROR,
                            Strings.ERROR_LAUNCHING_BROWSER_TITLE,
                            _makeTroubleshootingMessage(message)
                        );

                        _openDeferred.reject("OPEN_LIVE_BROWSER");
                    });
            }

            if (exports.status !== STATUS_ERROR) {
                window.setTimeout(function retryConnect() {
                    Inspector.connectToURL(launcherUrl).fail(onConnectFail);
                }, 3000);
            }
        });
    }

    // helper function that actually does the launch once we are sure we have
    // a doc and the server for that doc is up and running.
    function _doLaunchAfterServerReady(initialDoc) {
        // update status
        _setStatus(STATUS_CONNECTING);
        _createLiveDocumentForFrame(initialDoc);

        // start listening for requests
        _server.start();

        // Install a one-time event handler when connected to the launcher page
        Inspector.one("connect", _onConnect);

        // open browser to the interstitial page to prepare for loading agents
        _openInterstitialPage();

        // Once all agents loaded (see _onInterstitialPageLoad()), begin Live Highlighting for preprocessor documents
        _openDeferred.done(function () {
            // Setup activeEditorChange event listener so that we can track cursor positions in
            // CSS preprocessor files and perform live preview highlighting on all elements with
            // the current selector in the preprocessor file.
            EditorManager.on("activeEditorChange", onActiveEditorChange);

            // Explicitly trigger onActiveEditorChange so that live preview highlighting
            // can be set up for the preprocessor files.
            onActiveEditorChange(null, EditorManager.getActiveEditor(), null);
        });
    }

    function _prepareServer(doc) {
        var deferred = new $.Deferred(),
            showBaseUrlPrompt = false;

        _server = LiveDevServerManager.getServer(doc.file.fullPath);

        // Optionally prompt for a base URL if no server was found but the
        // file is a known server file extension
        showBaseUrlPrompt = !exports.config.experimental && !_server &&
            LiveDevelopmentUtils.isServerHtmlFileExt(doc.file.fullPath);

        if (showBaseUrlPrompt) {
            // Prompt for a base URL
            PreferencesDialogs.showProjectPreferencesDialog("", Strings.LIVE_DEV_NEED_BASEURL_MESSAGE)
                .done(function (id) {
                    if (id === Dialogs.DIALOG_BTN_OK && ProjectManager.getBaseUrl()) {
                        // If base url is specifed, then re-invoke _prepareServer() to continue
                        _prepareServer(doc).then(deferred.resolve, deferred.reject);
                    } else {
                        deferred.reject();
                    }
                });
        } else if (_server) {
            // Startup the server
            var readyPromise = _server.readyToServe();
            if (!readyPromise) {
                _showLiveDevServerNotReadyError();
                deferred.reject();
            } else {
                readyPromise.then(deferred.resolve, function () {
                    _showLiveDevServerNotReadyError();
                    deferred.reject();
                });
            }
        } else {
            // No server found
            deferred.reject();
        }

        return deferred.promise();
    }

    function getCurrentProjectServerConfig() {
        return {
            baseUrl: ProjectManager.getBaseUrl(),
            pathResolver: ProjectManager.makeProjectRelativeIfPossible,
            root: ProjectManager.getProjectRoot().fullPath
        };
    }

    function _createUserServer() {
        return new UserServer(getCurrentProjectServerConfig());
    }

    function _createFileServer() {
        return new FileServer(getCurrentProjectServerConfig());
    }
Private

_onDirtyFlagChange

Triggered by a change in dirty flag from the DocumentManager

    function _onDirtyFlagChange(event, doc) {
        if (doc && Inspector.connected() &&
                _server && agents.network && agents.network.wasURLRequested(_server.pathToUrl(doc.file.fullPath))) {
            // Set status to out of sync if dirty. Otherwise, set it to active status.
            _setStatus(_docIsOutOfSync(doc) ? STATUS_OUT_OF_SYNC : STATUS_ACTIVE);
        }
    }
Private

_onDisconnect

    function _onDisconnect(event) {
        _close(false, "closed_unknown_reason");
    }

    function _onDetached(event, res) {
        var closeReason;

        if (res && res.reason) {
            // Get the explanation from res.reason, e.g. "replaced_with_devtools", "target_closed", "canceled_by_user"
            // Examples taken from https://chromiumcodereview.appspot.com/10947037/patch/12001/13004
            // However, the link refers to the Chrome Extension API, it may not apply 100% to the Inspector API
            // Prefix with "detached_" to create a quasi-namespace for Chrome's reasons
            closeReason = "detached_" + res.reason;
        }

        _close(false, closeReason);
    }
Private

_onDocumentSaved

Triggered by a documentSaved event from DocumentManager.

event $.Event
doc Document
    function _onDocumentSaved(event, doc) {
        if (!Inspector.connected() || !_server) {
            return;
        }

        var absolutePath            = doc.file.fullPath,
            liveDocument            = absolutePath && _server.get(absolutePath),
            liveEditingEnabled      = liveDocument && liveDocument.isLiveEditingEnabled  && liveDocument.isLiveEditingEnabled();

        // Skip reload if the saved document has live editing enabled
        if (liveEditingEnabled) {
            return;
        }

        var documentUrl     = _server.pathToUrl(absolutePath),
            wasRequested    = agents.network && agents.network.wasURLRequested(documentUrl);

        if (wasRequested) {
            reload();
        }
    }
Private

_onError

Triggered by Inspector.error

    function _onError(event, error, msgData) {
        var message;

        // Sometimes error.message is undefined
        if (!error.message) {
            console.warn("Expected a non-empty string in error.message, got this instead:", error.message);
            message = JSON.stringify(error);
        } else {
            message = error.message;
        }

        // Remove "Uncaught" from the beginning to avoid the inspector popping up
        if (message && message.substr(0, 8) === "Uncaught") {
            message = message.substr(9);
        }

        // Additional information, like exactly which parameter could not be processed.
        var data = error.data;
        if (Array.isArray(data)) {
            message += "\n" + data.join("\n");
        }

        // Show the message, but include the error object for further information (e.g. error code)
        console.error(message, error, msgData);
    }

    function _styleSheetAdded(event, url) {
        var path = _server && _server.urlToPath(url),
            exists = !!_relatedDocuments[url];

        // path may be null if loading an external stylesheet.
        // Also, the stylesheet may already exist and be reported as added twice
        // due to Chrome reporting added/removed events after incremental changes
        // are pushed to the browser
        if (!path || exists) {
            return;
        }

        var docPromise = DocumentManager.getDocumentForPath(path);

        docPromise.done(function (doc) {
            if ((_classForDocument(doc) === CSSDocument ||
                    _classForDocument(doc) === CSSPreprocessorDocument) &&
                    (!_liveDocument || (doc !== _liveDocument.doc))) {
                // The doc may already have an editor (e.g. starting live preview from an css file),
                // so pass the editor if any
                var liveDoc = _createDocument(doc, doc._masterEditor);
                if (liveDoc) {
                    _server.add(liveDoc);
                    _relatedDocuments[doc.url] = liveDoc;

                    liveDoc.on("deleted.livedev", function (event, liveDoc) {
                        _closeRelatedDocument(liveDoc);
                    });
                }
            }
        });
    }
Private

_onFileChanged

    function _onFileChanged() {
        var doc = _getCurrentDocument();

        if (!doc || !Inspector.connected()) {
            return;
        }

        // close the current session and begin a new session if the current
        // document changes to an HTML document that was not loaded yet
        var docUrl = _server && _server.pathToUrl(doc.file.fullPath),
            wasRequested = agents.network && agents.network.wasURLRequested(docUrl),
            isViewable = exports.config.experimental || (_server && _server.canServe(doc.file.fullPath));

        if (!wasRequested && isViewable) {
            // Update status
            _setStatus(STATUS_CONNECTING);

            // clear live doc and related docs
            _closeDocuments();

            // create new live doc
            _createLiveDocumentForFrame(doc);

            // Navigate to the new page within this site. Agents must handle
            // frameNavigated event to clear any saved state.
            Inspector.Page.navigate(docUrl).then(function () {
                _setStatus(STATUS_ACTIVE);
            }, function () {
                _close(false, "closed_unknown_reason");
            });
        } else if (wasRequested) {
            // Update highlight
            showHighlight();
        }
    }
Private

_onInterstitialPageLoad

    function _onInterstitialPageLoad() {

        Inspector.Runtime.evaluate("window.navigator.userAgent", function (uaResponse) {
            Inspector.setUserAgent(uaResponse.result.value);
        });

        // Domains for some agents must be enabled first before loading
        var enablePromise = Inspector.Page.enable().then(function () {
            return Inspector.DOM.enable().then(_enableAgents, _enableAgents);
        });

        enablePromise.done(function () {
            // Some agents (e.g. DOMAgent and RemoteAgent) require us to
            // navigate to the page first before loading can complete.
            // To accomodate this, we load all agents and navigate in
            // parallel.

            // resolve/reject the open() promise after agents complete
            loadAgents().then(_openDeferred.resolve, _openDeferred.reject);

            _getInitialDocFromCurrent().done(function (doc) {
                if (doc && _liveDocument) {
                    if (doc !== _liveDocument.doc) {
                        _createLiveDocumentForFrame(doc);
                    }

                    // Navigate from interstitial to the document
                    // Fires a frameNavigated event
                    if (_server) {
                        Inspector.Page.navigate(_server.pathToUrl(doc.file.fullPath));
                    } else {
                        console.error("LiveDevelopment._onInterstitialPageLoad(): No server active");
                    }
                } else {
                    // Unlikely that we would get to this state where
                    // a connection is in process but there is no current
                    // document
                    close();
                }
            });
        });
    }
Private

_setStatus

Update the status. Triggers a statusChange event.

status number
new status
closeReason nullable string
Optional string key suffix to display to user when closing the live development connection (see LIVE_DEV_* keys)
    function _setStatus(status, closeReason) {
        // Don't send a notification when the status didn't actually change
        if (status === exports.status) {
            return;
        }

        exports.status = status;

        var reason = status === STATUS_INACTIVE ? closeReason : null;
        exports.trigger("statusChange", status, reason);
    }
Private

_waitForInterstitialPageLoad

Returns: jQuery.Promise
Resolves once page is loaded
    function _waitForInterstitialPageLoad() {
        var deferred    = $.Deferred(),
            keepPolling = true,
            timer       = window.setTimeout(function () {
                keepPolling = false;
                deferred.reject();
            }, 10000); // 10 seconds

cleanup

Finish closing the live development connection, including setting the status accordingly.

        function cleanup() {
            // Need to do this in order to trigger the corresponding CloseLiveBrowser cleanups required on
            // the native Mac side
            var closeDeferred = (brackets.platform === "mac") ? NativeApp.closeLiveBrowser() : $.Deferred().resolve();
            closeDeferred.done(function () {
                _setStatus(STATUS_INACTIVE, reason || "explicit_close");
                // clean-up registered servers
                _regServers.forEach(function (server) {
                    LiveDevServerManager.removeServer(server);
                });
                _regServers = [];
                _closeDeferred.resolve();
            }).fail(function (err) {
                if (err) {
                    reason +=  " (" + err + ")";
                }
                _setStatus(STATUS_INACTIVE, reason || "explicit_close");
                _closeDeferred.resolve();
            });
        }

        if (_isPromisePending(_openDeferred)) {
            // Reject calls to open if requests are still pending
            _openDeferred.reject();
        }

        if (exports.status === STATUS_INACTIVE) {
            // Ignore close if status is inactive
            _closeDeferred.resolve();
        } else {
            _doInspectorDisconnect(doCloseWindow).always(cleanup);
        }

        return promise;
    }

    // WebInspector Event: Page.frameNavigated
    function _onFrameNavigated(event, res) {
        // res = {frame}
        var url = res.frame.url,
            baseUrl,
            baseUrlRegExp;

        // Only check domain of root frame (with undefined parentId)
        if (res.frame.parentId) {
            return;
        }

        // Any local file is OK
        if (url.match(/^file:\/\//i) || !_server) {
            return;
        }

        // Need base url to build reg exp
        baseUrl = _server.getBaseUrl();
        if (!baseUrl) {
            return;
        }

        // Test that url is within site
        baseUrlRegExp = new RegExp("^" + StringUtils.regexEscape(baseUrl), "i");
        if (!url.match(baseUrlRegExp)) {
            // No longer in site, so terminate live dev, but don't close browser window
            _close(false, "navigated_away");
        }
    }
Public API

close

Close the connection and the associated window asynchronously

Returns: jQuery.Promise
Resolves once the connection is closed
    function close() {
        return _close(true);
    }
Public API

disableAgent

Disable an agent. Takes effect next time a connection is made. Does not affect current live development sessions.

@param {string} name of agent to disable

    function disableAgent(name) {
        if (_enabledAgentNames.hasOwnProperty(name)) {
            delete _enabledAgentNames[name];
        }
    }
Public API

enableAgent

Enable an agent. Takes effect next time a connection is made. Does not affect current live development sessions.

@param {string} name of agent to enable

    function enableAgent(name) {
        if (agents.hasOwnProperty(name) && !_enabledAgentNames.hasOwnProperty(name)) {
            _enabledAgentNames[name] = true;
        }
    }
Public API

hideHighlight

Hide any active highlighting

    function hideHighlight() {
        if (Inspector.connected() && agents.highlight) {
            agents.highlight.hide();
        }
    }
Public API

init

Initialize the LiveDevelopment Session

    function init(theConfig) {
        exports.config = theConfig;

        Inspector.on("error", _onError);
        Inspector.Inspector.on("detached", _onDetached);

        // Only listen for styleSheetAdded
        // We may get interim added/removed events when pushing incremental updates
        CSSAgent.on("styleSheetAdded.livedev", _styleSheetAdded);

        MainViewManager
            .on("currentFileChange", _onFileChanged);
        DocumentManager
            .on("documentSaved", _onDocumentSaved)
            .on("dirtyFlagChange", _onDirtyFlagChange);
        ProjectManager
            .on("beforeProjectClose beforeAppClose", close);

        // Initialize exports.status
        _setStatus(STATUS_INACTIVE);
    }

    function _getServer() {
        return _server;
    }

    function getServerBaseUrl() {
        return _server && _server.getBaseUrl();
    }


    EventDispatcher.makeEventDispatcher(exports);

    // For unit testing
    exports.launcherUrl               = launcherUrl;
    exports._getServer                = _getServer;
    exports._getInitialDocFromCurrent = _getInitialDocFromCurrent;

    // Export public functions
    exports.agents              = agents;
    exports.open                = open;
    exports.close               = close;
    exports.reconnect           = reconnect;
    exports.reload              = reload;
    exports.enableAgent         = enableAgent;
    exports.disableAgent        = disableAgent;
    exports.getLiveDocForPath   = getLiveDocForPath;
    exports.showHighlight       = showHighlight;
    exports.hideHighlight       = hideHighlight;
    exports.redrawHighlight     = redrawHighlight;
    exports.init                = init;
    exports.getCurrentProjectServerConfig = getCurrentProjectServerConfig;
    exports.getServerBaseUrl    = getServerBaseUrl;
});

loadAgents

Load the agents

    function loadAgents() {
        // If we're already loading agents return same promise
        if (_loadAgentsPromise) {
            return _loadAgentsPromise;
        }

        var result = new $.Deferred(),
            allAgentsPromise;

        _loadAgentsPromise = result.promise();

        _setStatus(STATUS_LOADING_AGENTS);

        // load agents in parallel
        allAgentsPromise = Async.doInParallel(
            getEnabledAgents(),
            function (name) {
                return _invokeAgentMethod(name, "load").done(function () {
                    _loadedAgentNames.push(name);
                });
            },
            true
        );

        // wrap agent loading with a timeout
        allAgentsPromise = Async.withTimeout(allAgentsPromise, 10000);

        allAgentsPromise.done(function () {
            var doc = (_liveDocument) ? _liveDocument.doc : null;

            if (doc) {
                var status = STATUS_ACTIVE;

                if (_docIsOutOfSync(doc)) {
                    status = STATUS_OUT_OF_SYNC;
                }

                _setStatus(status);
                result.resolve();
            } else {
                result.reject();
            }
        });

        allAgentsPromise.fail(result.reject);

        _loadAgentsPromise
            .fail(function () {
                // show error loading live dev dialog
                _setStatus(STATUS_ERROR);

                Dialogs.showModalDialog(
                    Dialogs.DIALOG_ID_ERROR,
                    Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
                    _makeTroubleshootingMessage(Strings.LIVE_DEV_LOADING_ERROR_MESSAGE)
                );
            })
            .always(function () {
                _loadAgentsPromise = null;
            });

        return _loadAgentsPromise;
    }

onActiveEditorChange

If the current editor is for a CSS preprocessor file, then add it to the style sheet so that we can track cursor positions in the editor to show live preview highlighting. For normal CSS we only do highlighting from files we know for sure are referenced by the current live preview document, but for preprocessors we just assume that any preprocessor file you edit is probably related to the live preview.

event Event
(unused)
current Editor
Current editor
previous Editor
Previous editor
    function onActiveEditorChange(event, current, previous) {
        if (previous && previous.document &&
                CSSUtils.isCSSPreprocessorFile(previous.document.file.fullPath)) {
            var prevDocUrl = _server && _server.pathToUrl(previous.document.file.fullPath);

            if (_relatedDocuments && _relatedDocuments[prevDocUrl]) {
                _closeRelatedDocument(_relatedDocuments[prevDocUrl]);
            }
        }
        if (current && current.document &&
                CSSUtils.isCSSPreprocessorFile(current.document.file.fullPath)) {
            var docUrl = _server && _server.pathToUrl(current.document.file.fullPath);
            _styleSheetAdded(null, docUrl);
        }
    }
Public API

open

Open the Connection and go live

restart non-nullable boolean
true if relaunching and _openDeferred already exists
Returns: jQuery.Promise
Resolves once live preview is open
    function open(restart) {
        // If close() is still pending, wait for close to finish before opening
        if (_isPromisePending(_closeDeferred)) {
            return _closeDeferred.then(function () {
                return open(restart);
            });
        }

        if (!restart) {
            // Return existing promise if it is still pending
            if (_isPromisePending(_openDeferred)) {
                return _openDeferred;
            } else {
                _openDeferred = new $.Deferred();
                _openDeferred.always(function () {
                    _openDeferred = null;
                });
            }
        }
        // Send analytics data when Live Preview is opened
        HealthLogger.sendAnalyticsData(
            "livePreviewOpen",
            "usage",
            "livePreview",
            "open"
        );

        // Register user defined server provider and keep handlers for further clean-up
        _regServers.push(LiveDevServerManager.registerServer({ create: _createUserServer }, 99));
        _regServers.push(LiveDevServerManager.registerServer({ create: _createFileServer }, 0));

        // TODO: need to run _onFileChanged() after load if doc != currentDocument here? Maybe not, since activeEditorChange
        // doesn't trigger it, while inline editors can still cause edits in doc other than currentDoc...
        _getInitialDocFromCurrent().done(function (doc) {
            var prepareServerPromise = (doc && _prepareServer(doc)) || new $.Deferred().reject(),
                otherDocumentsInWorkingFiles;

            if (doc && !doc._masterEditor) {
                otherDocumentsInWorkingFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES).length;
                MainViewManager.addToWorkingSet(MainViewManager.ACTIVE_PANE, doc.file);

                if (!otherDocumentsInWorkingFiles) {
                    MainViewManager._edit(MainViewManager.ACTIVE_PANE, doc);
                }
            }

            // wait for server (StaticServer, Base URL or file:)
            prepareServerPromise
                .done(function () {
                    var reverseInspectPref = PreferencesManager.get("livedev.enableReverseInspect"),
                        wsPort             = PreferencesManager.get("livedev.wsPort");
                        
                    if (wsPort && reverseInspectPref) {
                        WebSocketTransport.createWebSocketServer(wsPort);
                    }
                    _doLaunchAfterServerReady(doc);
                })
                .fail(function () {
                    _showWrongDocError();
                });
        });

        return _openDeferred.promise();
    }

pollInterstitialPage

Asynchronously check to see if the interstitial page has finished loading; if not, check again until timing out.

        function pollInterstitialPage() {
            if (keepPolling && Inspector.connected()) {
                Inspector.Runtime.evaluate("window.isBracketsLiveDevelopmentInterstitialPageLoaded", function (response) {
                    var result = response.result;

                    if (result.type === "boolean" && result.value) {
                        window.clearTimeout(timer);
                        deferred.resolve();
                    } else {
                        window.setTimeout(pollInterstitialPage, 100);
                    }
                });
            } else {
                deferred.reject();
            }
        }

        pollInterstitialPage();
        return deferred.promise();
    }
Public API

reconnect

Unload and reload agents

Returns: jQuery.Promise
Resolves once the agents are loaded
    function reconnect() {
        if (_loadAgentsPromise) {
            // Agents are already loading, so don't unload
            return _loadAgentsPromise;
        }

        unloadAgents();

        // Clear any existing related documents before we reload the agents.
        // We need to recreate them for the reloaded document due to some
        // desirable side-effects (see #7606). Eventually, we should simplify
        // the way we get that behavior.
        _.forOwn(_relatedDocuments, function (relatedDoc) {
            _closeRelatedDocument(relatedDoc);
        });

        return loadAgents();
    }
Public API

redrawHighlight

Redraw highlights *

    function redrawHighlight() {
        if (Inspector.connected() && agents.highlight) {
            agents.highlight.redraw();
        }
    }
Public API

reload

reload the live preview

    function reload() {
        // Unload and reload agents before reloading the page
        // Some agents (e.g. DOMAgent and RemoteAgent) require us to
        // navigate to the page first before loading can complete.
        // To accomodate this, we load all agents (in reconnect())
        // and navigate in parallel.
        reconnect();

        // Reload HTML page
        Inspector.Page.reload();
    }
Public API

showHighlight

Enable highlighting

    function showHighlight() {
        var doc = getLiveDocForEditor(EditorManager.getActiveEditor());

        if (doc && doc.updateHighlight) {
            doc.updateHighlight();
        }
    }

unloadAgents

Unload the agents

    function unloadAgents() {
        _loadedAgentNames.forEach(function (name) {
            agents[name].unload();
        });
        _loadedAgentNames = [];
    }