LiveDevelopment allows Brackets to launch a browser with a "live preview" that's connected to the current editor.
To start a session call open
. This will read the currentDocument from brackets,
launch it in the default browser, and connect to it for live editing.
To stop a session call close
. This will close the connection to the browser
(but will not close the browser tab).
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:
0: Inactive 1: Connecting (waiting for a browser connection) 2: Active 3: Out of sync 4: Sync error 5: Reloading (after saving JS changes) 6: Restarting (switching context to a new HTML live doc)
The reason codes are:
function _classForDocument(doc) {
if (doc.getLanguage().getId() === "css") {
return LiveCSSDocument;
}
if (LiveDevelopmentUtils.isHtmlFileExt(doc.file.fullPath)) {
return LiveHTMLDocument;
}
return null;
}
function _close(doCloseWindow, reason) {
if (exports.status !== STATUS_INACTIVE) {
// Close live documents
_closeDocuments();
// Close all active connections
_protocol.closeAllConnections();
if (_server) {
// Stop listening for requests when disconnected
_server.stop();
// Dispose server
_server = null;
}
}
//TODO: implement closeWindow together with launchers.
// if (doCloseWindow) {
//
// }
_setStatus(STATUS_INACTIVE, reason || "explicit_close");
}
function _closeDocument(liveDocument) {
liveDocument.off(".livedev");
liveDocument.close();
}
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();
}
}
function _createLiveDocument(doc, editor, roots) {
var DocClass = _classForDocument(doc),
liveDocument;
if (!DocClass) {
return null;
}
liveDocument = new DocClass(_protocol, _resolveUrl, doc, editor, roots);
liveDocument.on("errorStatusChanged.livedev", function (event, hasErrors) {
if (isActive()) {
_setStatus(hasErrors ? STATUS_SYNC_ERROR : STATUS_ACTIVE);
}
});
return liveDocument;
}
function _createLiveDocumentForFrame(doc) {
// create live document
doc._ensureMasterEditor();
_liveDocument = _createLiveDocument(doc, doc._masterEditor);
_server.add(_liveDocument);
}
function _doLaunchAfterServerReady(initialDoc) {
_createLiveDocumentForFrame(initialDoc);
// start listening for requests
_server.start();
// open browser to the url
_open(initialDoc);
}
Documents are considered to be out-of-sync if they are dirty and do not have "update while editing" support
function _docIsOutOfSync(doc) {
var liveDoc = _server && _server.get(doc.file.fullPath),
isLiveEditingEnabled = liveDoc && liveDoc.isLiveEditingEnabled();
return doc.isDirty && !isLiveEditingEnabled;
}
function _getInitialDocFromCurrent() {
var doc = DocumentManager.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();
}
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 _handleRelatedDocumentDeleted(url) {
var liveDoc = _relatedDocuments[url];
if (liveDoc) {
delete _relatedDocuments[url];
}
if (_server) {
_server.remove(liveDoc);
}
_closeDocument(liveDoc);
}
Launches the given URL in the default browser.
function _launch(url) {
// open default browser
// TODO: fail?
//
_launcher.launch(url);
}
For files that don't support as-you-type live editing, but are loaded by live HTML documents (e.g. JS files), we want to show a dirty indicator on the live development icon when they have unsaved changes, so the user knows s/he needs to save in order to have the page reload.
function _onDirtyFlagChange(event, doc) {
if (!isActive() || !_server) {
return;
}
var absolutePath = doc.file.fullPath;
if (_liveDocument.isRelated(absolutePath)) {
// Set status to out of sync if dirty. Otherwise, set it to active status.
_setStatus(_docIsOutOfSync(doc) ? STATUS_OUT_OF_SYNC : STATUS_ACTIVE);
}
}
For files that don't support as-you-type live editing, but are loaded by live HTML documents (e.g. JS files), we want to reload the full document when they're saved.
function _onDocumentSaved(event, doc) {
if (!isActive() || !_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;
}
// reload the page if the given document is a JS file related
// to the current live document.
if (_liveDocument.isRelated(absolutePath)) {
if (doc.getLanguage().getId() === "javascript") {
_setStatus(STATUS_RELOADING);
_protocol.reload();
}
}
}
function _onFileChange() {
var doc = DocumentManager.getCurrentDocument();
if (!isActive() || !doc) {
return;
}
// close the current session and begin a new session
var docUrl = _server && _server.pathToUrl(doc.file.fullPath),
isViewable = _server && _server.canServe(doc.file.fullPath);
if (_liveDocument.doc.url !== docUrl && isViewable) {
// clear live doc and related docs
_closeDocuments();
// create new live doc
_createLiveDocumentForFrame(doc);
_setStatus(STATUS_RESTARTING);
_open(doc);
}
}
function _open(doc) {
if (doc && _liveDocument && doc === _liveDocument.doc) {
if (_server) {
// Launch the URL in the browser. If it's the first one to connect back to us,
// our status will transition to ACTIVE once it does so.
if (exports.status < STATUS_ACTIVE) {
_launch(_server.pathToUrl(doc.file.fullPath));
}
if (exports.status === STATUS_RESTARTING) {
// change page in browser
_protocol.navigate(_server.pathToUrl(doc.file.fullPath));
}
_protocol
// TODO: timeout if we don't get a connection within a certain time
.on("ConnectionConnect.livedev", function (event, msg) {
// check for the first connection
if (_protocol.getConnectionIds().length === 1) {
// check the page that connection comes from matches the current live document session
if (_liveDocument && (msg.url === _resolveUrl(_liveDocument.doc.file.fullPath))) {
_setStatus(STATUS_ACTIVE);
}
}
})
.on("ConnectionClose.livedev", function (event, msg) {
// close session when the last connection was closed
if (_protocol.getConnectionIds().length === 0) {
setTimeout(function () {
if (_protocol.getConnectionIds().length === 0 &&
exports.status <= STATUS_ACTIVE) {
_close(false, "detached_target_closed");
}
}, 5000);
}
})
// extract stylesheets and create related LiveCSSDocument instances
.on("DocumentRelated.livedev", function (event, msg) {
var relatedDocs = msg.related;
var docs = Object.keys(relatedDocs.stylesheets);
docs.forEach(function (url) {
_styleSheetAdded(null, url, relatedDocs.stylesheets[url]);
});
})
// create new LiveCSSDocument if a new stylesheet is added
.on("StylesheetAdded.livedev", function (event, msg) {
_styleSheetAdded(null, msg.href, msg.roots);
})
// remove LiveCSSDocument instance when stylesheet is removed
.on("StylesheetRemoved.livedev", function (event, msg) {
_handleRelatedDocumentDeleted(msg.href);
});
} else {
console.error("LiveDevelopment._open(): 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();
}
}
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 = !_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 _resolveUrl(path) {
return _server && _server.pathToUrl(path);
}
Update the status. Triggers a statusChange event.
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);
}
function _showLiveDevServerNotReadyError() {
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
Strings.LIVE_DEV_SERVER_NOT_READY_MESSAGE
);
}
function _showWrongDocError() {
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
Strings.LIVE_DEV_NEED_HTML_MESSAGE
);
}
Handles a notification from the browser that a stylesheet was loaded into the live HTML document. If the stylesheet maps to a file in the project, then creates a live document for the stylesheet and adds it to _relatedDocuments.
function _styleSheetAdded(event, url, roots) {
var path = _server && _server.urlToPath(url),
alreadyAdded = !!_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 || alreadyAdded) {
return;
}
var docPromise = DocumentManager.getDocumentForPath(path);
docPromise.done(function (doc) {
if ((_classForDocument(doc) === LiveCSSDocument) &&
(!_liveDocument || (doc !== _liveDocument.doc))) {
var liveDoc = _createLiveDocument(doc, doc._masterEditor, roots);
if (liveDoc) {
_server.add(liveDoc);
_relatedDocuments[doc.url] = liveDoc;
liveDoc.on("updateDoc", function (event, url) {
var path = _server.urlToPath(url),
doc = getLiveDocForPath(path);
doc._updateBrowser();
});
}
}
});
}
Closes all active connections. Returns a resolved promise for API compatibility.
function close() {
_close(true);
return new $.Deferred().resolve().promise();
}
Returns current project server config. Copied from original LiveDevelopment.
function getCurrentProjectServerConfig() {
return {
baseUrl: ProjectManager.getBaseUrl(),
pathResolver: ProjectManager.makeProjectRelativeIfPossible,
root: ProjectManager.getProjectRoot().fullPath
};
}
Returns the live document for a given path, or null if there is no live document for it.
function getLiveDocForPath(path) {
if (!_server) {
return null;
}
return _server.get(path);
}
function getServerBaseUrl() {
return _server && _server.getBaseUrl();
}
// for unit testing only
function _getCurrentLiveDoc() {
return _liveDocument;
}
EventDispatcher.makeEventDispatcher(exports);
// For unit testing
exports._server = _server;
exports._getCurrentLiveDoc = _getCurrentLiveDoc;
exports._getInitialDocFromCurrent = _getInitialDocFromCurrent;
// Export public functions
exports.open = open;
exports.close = close;
exports.reconnect = reconnect;
exports.reload = reload;
exports.getLiveDocForPath = getLiveDocForPath;
exports.showHighlight = showHighlight;
exports.hideHighlight = hideHighlight;
exports.redrawHighlight = redrawHighlight;
exports.init = init;
exports.isActive = isActive;
exports.getServerBaseUrl = getServerBaseUrl;
exports.getCurrentProjectServerConfig = getCurrentProjectServerConfig;
exports.setTransport = setTransport;
exports.setLauncher = setLauncher;
});
Hide any active highlighting
function hideHighlight() {
if (_protocol) {
_protocol.evaluate("_LD.hideHighlight()");
}
}
Initialize the LiveDevelopment module.
function init(config) {
exports.config = config;
MainViewManager
.on("currentFileChange", _onFileChange);
DocumentManager
.on("documentSaved", _onDocumentSaved)
.on("dirtyFlagChange", _onDirtyFlagChange);
ProjectManager
.on("beforeProjectClose beforeAppClose", close);
// Default transport for live connection messages - can be changed
setTransport(NodeSocketTransport);
// Default launcher for preview browser - can be changed
setLauncher(DefaultLauncher);
// Initialize exports.status
_setStatus(STATUS_INACTIVE);
}
function getLiveDocForEditor(editor) {
if (!editor) {
return null;
}
return getLiveDocForPath(editor.document.file.fullPath);
}
Returns true if the global Live Development mode is on (might be in the middle of connecting).
function isActive() {
return exports.status > STATUS_INACTIVE;
}
Open a live preview on the current docuemnt.
function open() {
// TODO: need to run _onDocumentChange() 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.getWorkingSetSize(MainViewManager.ALL_PANES);
MainViewManager.addToWorkingSet(MainViewManager.ACTIVE_PANE, doc.file);
if (!otherDocumentsInWorkingFiles) {
CommandManager.execute(Commands.CMD_OPEN, { fullPath: doc.file.fullPath });
}
}
// wait for server (StaticServer, Base URL or file:)
prepareServerPromise
.done(function () {
_setStatus(STATUS_CONNECTING);
_doLaunchAfterServerReady(doc);
})
.fail(function () {
_showWrongDocError();
});
});
}
Originally unload and reload agents. It doesn't apply for this new implementation.
function reconnect() {
return $.Deferred().resolve();
}
Redraw highlights
function redrawHighlight() {
if (_protocol) {
_protocol.evaluate("_LD.redrawHighlights()");
}
}
Reload current page in all connected browsers.
function reload() {
if (_protocol) {
_protocol.reload();
}
}
Sets the current browser launcher mechanism to be used by live development (e.g., default browser, iframe-based browser, etc.) The launcher must provide the following method:
function setLauncher(launcher) {
if (!(launcher && launcher.launch)) {
console.log("Invalid launcher object: ", launcher, new Error("LiveDevMultiBrowser.setLauncher()"));
return;
}
_launcher = launcher;
}
Sets the current transport mechanism to be used by the live development protocol (e.g. socket server, iframe postMessage, etc.) The low-level transport. Must provide the following methods:
It must also dispatch the following jQuery events:
function setTransport(transport) {
_protocol.setTransport(transport);
}