From 84f6605a0288e2dcd8936978fbf9038229a661ea Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Tue, 18 Nov 2014 14:23:51 -0300 Subject: [PATCH 01/17] Add multi-browser LiveDevelopment (experimental) --- src/LiveDevelopment/LiveDevMultiBrowser.js | 844 +++++++++++++++++ .../MultiBrowserImpl/README.md | 83 ++ .../documents/LiveCSSDocument.js | 172 ++++ .../documents/LiveDocument.js | 341 +++++++ .../documents/LiveHTMLDocument.js | 308 ++++++ .../language/HTMLInstrumentation.js | 876 ++++++++++++++++++ .../language/HTMLSimpleDOM.js | 558 +++++++++++ src/LiveDevelopment/MultiBrowserImpl/main.js | 214 +++++ .../protocol/LiveDevProtocol.js | 319 +++++++ .../protocol/remote/DocumentObserver.js | 309 ++++++ .../remote/ExtendedRemoteFunctions.js | 63 ++ .../protocol/remote/LiveDevProtocolRemote.js | 283 ++++++ .../transports/NodeSocketTransport.js | 89 ++ .../node/NodeSocketTransportDomain.js | 249 +++++ .../transports/node/package.json | 8 + .../remote/NodeSocketTransportRemote.js | 138 +++ src/LiveDevelopment/main.js | 19 +- 17 files changed, 4870 insertions(+), 3 deletions(-) create mode 100644 src/LiveDevelopment/LiveDevMultiBrowser.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/README.md create mode 100644 src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/language/HTMLSimpleDOM.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/main.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json create mode 100644 src/LiveDevelopment/MultiBrowserImpl/transports/remote/NodeSocketTransportRemote.js diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js new file mode 100644 index 00000000000..8988a339df8 --- /dev/null +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -0,0 +1,844 @@ +/* + * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ +/*global define, $, brackets, window, open */ + +/** + * LiveDevelopment allows Brackets to launch a browser with a "live preview" that's + * connected to the current editor. + * + * # STARTING + * + * 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. + * + * # STOPPING + * + * To stop a session call `close`. This will close the connection to the browser + * (but will not close the browser tab). + * + * # 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 (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: + * - 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) + */ +define(function (require, exports, module) { + "use strict"; + + // Status Codes + var STATUS_ERROR = exports.STATUS_ERROR = -1; + var STATUS_INACTIVE = exports.STATUS_INACTIVE = 0; + var STATUS_CONNECTING = exports.STATUS_CONNECTING = 1; + var STATUS_ACTIVE = exports.STATUS_ACTIVE = 2; + var STATUS_OUT_OF_SYNC = exports.STATUS_OUT_OF_SYNC = 3; + var STATUS_SYNC_ERROR = exports.STATUS_SYNC_ERROR = 4; + var STATUS_RELOADING = exports.STATUS_RELOADING = 5; + var STATUS_RESTARTING = exports.STATUS_RESTARTING = 6; + + var Async = require("utils/Async"), + Dialogs = require("widgets/Dialogs"), + DefaultDialogs = require("widgets/DefaultDialogs"), + DocumentManager = require("document/DocumentManager"), + EditorManager = require("editor/EditorManager"), + ExtensionUtils = require("utils/ExtensionUtils"), + FileSystemError = require("filesystem/FileSystemError"), + FileUtils = require("file/FileUtils"), + PreferencesDialogs = require("preferences/PreferencesDialogs"), + ProjectManager = require("project/ProjectManager"), + Strings = require("strings"), + _ = require("thirdparty/lodash"), + LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"), + NodeSocketTransport = require("LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport"), + LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"); + + // Documents + var LiveCSSDocument = require("LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument"), + LiveHTMLDocument = require("LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument"); + + /** + * @private + * The live HTML document for the currently active preview. + * @type {LiveHTMLDocument} + */ + var _liveDocument; + + /** + * @private + * Live documents related to the active HTML document - for example, CSS files + * that are used by the document. + * @type {Object.} + */ + var _relatedDocuments = {}; + + /** + * @private + * Current transport for communicating with browser instances. See setTransport(). + * @type {{launch: function(string), send: function(number|Array., string), close: function(number)}} + */ + var _transport; + + /** + * @private + * Protocol handler that provides the actual live development API on top of the current transport. + */ + var _protocol = LiveDevProtocol; + + /** + * @private + * Current live preview server + * @type {BaseServer} + */ + var _server; + + /** + * @private + * Returns true if we think the given extension is for an HTML file. + * @param {string} ext The extension to check. + * @return {boolean} true if this is an HTML extension + */ + function _isHtmlFileExt(ext) { + return (FileUtils.isStaticHtmlFileExt(ext) || + (ProjectManager.getBaseUrl() && FileUtils.isServerHtmlFileExt(ext))); + } + + /** + * @private + * Determine which live document class should be used for a given document + * @param {Document} document The document we want to create a live document for. + * @return {function} The constructor for the live document class; will be a subclass of LiveDocument. + */ + function _classForDocument(doc) { + if (doc.getLanguage().getId() === "css") { + return LiveCSSDocument; + } + + if (_isHtmlFileExt(doc.file.fullPath)) { + return LiveHTMLDocument; + } + + return null; + } + + /** + * Returns true if the global Live Development mode is on (might be in the middle of connecting). + * @return {boolean} + */ + function isActive() { + return exports.status > STATUS_INACTIVE; + } + + /** + * Returns the live document for a given path, or null if there is no live document for it. + * @param {string} path + * @return {?LiveDocument} + */ + function getLiveDocForPath(path) { + if (!_server) { + return null; + } + + return _server.get(path); + } + + /** + * Returns the live document for a given editor, or null if there is no live document for it. + * @param {Editor} editor + * @return {?LiveDocument} + */ + function getLiveDocForEditor(editor) { + if (!editor) { + return null; + } + return getLiveDocForPath(editor.document.file.fullPath); + } + + /** + * @private + * Close a live document. + * @param {LiveDocument} + */ + function _closeDocument(liveDocument) { + $(liveDocument).off(".livedev"); + liveDocument.close(); + } + + /** + * 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). + * @param {string} url Absolute URL of the related document + */ + function _handleRelatedDocumentDeleted(url) { + var liveDoc = _relatedDocuments[url]; + if (liveDoc) { + delete _relatedDocuments[url]; + } + + if (_server) { + _server.remove(liveDoc); + } + _closeDocument(liveDoc); + } + + /** + * Update the status. Triggers a statusChange event. + * @param {number} status new status + * @param {?string} closeReason 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).triggerHandler("statusChange", [status, reason]); + } + + /** + * @private + * Close all live documents. + */ + 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 + * Returns the URL that we would serve the given path at. + * @param {string} path + * @return {string} + */ + function _resolveUrl(path) { + return _server && _server.pathToUrl(path); + } + + /** + * @private + * Create a LiveDocument for a Brackets editor/document to manage communication between the + * editor and the browser. + * @param {Document} doc + * @param {Editor} editor + * @param {roots} roots + * @return {?LiveDocument} The live document, or null if this type of file doesn't support live editing. + */ + 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; + } + + /** + * Documents are considered to be out-of-sync if they are dirty and + * do not have "update while editing" support + * @param {Document} doc + * @return {boolean} + */ + function _docIsOutOfSync(doc) { + var liveDoc = _server && _server.get(doc.file.fullPath), + isLiveEditingEnabled = liveDoc && liveDoc.isLiveEditingEnabled(); + + return doc.isDirty && !isLiveEditingEnabled; + } + + /** + * 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. + * @param {$.Event} event + * @param {string} url The URL of the stylesheet that was added. + * @param {array} roots The URLs of the roots of the stylesheet (the css files loaded through ) + */ + 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, null, 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(); + }); + } + } + }); + } + + /** + * @private + * Determine an index file that can be used to start Live Development. + * This function will inspect all files in a project to find the closest index file + * available for currently opened document. We are searching for these files: + * - index.html + * - index.htm + * + * If the project is configured with a custom base url for live developmment, then + * the list of possible index files is extended to contain these index files too: + * - index.php + * - index.php3 + * - index.php4 + * - index.php5 + * - index.phtm + * - index.phtml + * - index.cfm + * - index.cfml + * - index.asp + * - index.aspx + * - index.jsp + * - index.jspx + * - index.shm + * - index.shml + * + * If a file was found, the promise will be resolved with the full path to this file. If no file + * was found in the whole project tree, the promise will be resolved with null. + * + * @return {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 = 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 (FileUtils.isStaticHtmlFileExt(refPath) || FileUtils.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.getDirectoryPath(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 ((FileUtils.isServerHtmlFileExt(fileInfo.name)) || + (FileUtils.isStaticHtmlFileExt(fileInfo.name))) { + return true; + } + } else if (FileUtils.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.getDirectoryPath(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 + * Close the connection and the associated window + * @param {boolean} doCloseWindow Use true to close the window/tab in the browser + * @param {?string} reason Optional string key suffix to display to user (see LIVE_DEV_* keys) + */ + 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: don't have a way to close windows in the new architecture +// if (doCloseWindow) { +// } + + _setStatus(STATUS_INACTIVE, reason || "explicit_close"); + } + + /** + * Close all active connections + */ + function close() { + return _close(true); + } + + /** + * @private + * Displays an error when no HTML file can be found to preview. + */ + function _showWrongDocError() { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.LIVE_DEVELOPMENT_ERROR_TITLE, + Strings.LIVE_DEV_NEED_HTML_MESSAGE + ); + } + + /** + * @private + * Displays an error when the server for live development files can't be started. + */ + function _showLiveDevServerNotReadyError() { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.LIVE_DEVELOPMENT_ERROR_TITLE, + Strings.LIVE_DEV_SERVER_NOT_READY_MESSAGE + ); + } + + /** + * @private + * Creates the main live document for a given HTML document and notifies the server it exists. + * TODO: we should really maintain the list of live documents, not the server. + * @param {Document} doc + */ + function _createLiveDocumentForFrame(doc) { + // create live document + doc._ensureMasterEditor(); + _liveDocument = _createLiveDocument(doc, doc._masterEditor); + _server.add(_liveDocument); + } + + /** + * @private + * Launches the given document in the browser, given that a live document has already + * been created for it. + * @param {Document} 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) { + _protocol.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("Connection.connect.livedev", function (event, msg) { + // check for the first connection + if (_protocol.getConnectionIds().length === 1) { + var doc = DocumentManager.getCurrentDocument(); + // 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("Connection.close.livedev", function (event, msg) { + // close session when the last connection was closed + if (_protocol.getConnectionIds().length === 0) { + if (exports.status <= STATUS_ACTIVE) { + _close(false, "detached_target_closed"); + } + } + }) + // extract stylesheets and create related LiveCSSDocument instances + .on("Document.Related.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("Stylesheet.Added.livedev", function (event, msg) { + _styleSheetAdded(null, msg.href, msg.roots); + }) + // remove LiveCSSDocument instance when stylesheet is removed + .on("Stylesheet.Removed.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(); + } + } + + /** + * @private + * Creates the live document in preparation for launching the + * preview of the given document, then launches it. (The live document + * must already exist before we launch it so that the server can + * ask it for the instrumented version of the document when the browser + * requests it.) + * TODO: could probably just consolidate this with _open() + * @param {Document} doc + */ + function _doLaunchAfterServerReady(initialDoc) { + + _createLiveDocumentForFrame(initialDoc); + + // start listening for requests + _server.start(); + + // open browser to the url + _open(initialDoc); + } + + /** + * @private + * Create the server in preparation for opening a live preview. + * @param {Document} doc The document we want the server for. Different servers handle + * different types of project (a static server for when no app server is configured, + * vs. a user server when there is an app server set in File > Project Settings). + */ + 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 && FileUtils.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(); + } + + /** + * @private + * When switching documents, close the current preview and open a new one. + */ + function _onDocumentChange() { + 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); + + } + } + + + /** + * 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 = DocumentManager.getWorkingSet().length; + DocumentManager.addToWorkingSet(doc.file); + + if (!otherDocumentsInWorkingFiles) { + DocumentManager.setCurrentDocument(doc); + } + } + + // wait for server (StaticServer, Base URL or file:) + prepareServerPromise + .done(function () { + _setStatus(STATUS_CONNECTING); + _doLaunchAfterServerReady(doc); + }) + .fail(function () { + _showWrongDocError(); + }); + }); + } + + /** + * 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. + * @param {$.Event} event + * @param {Document} doc + */ + 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(); + } + } + } + + /** + * 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. + * @param {$.Event} event + * @param {Document} doc + */ + 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); + } + } + + /** + * Sets the current transport mechanism to be used by the live development protocol + * (e.g. socket server, iframe postMessage, etc.) + * @param {{launch: function(string), send: function(number|Array., string), close: function(number), getRemoteScript: function(): ?string}} transport + * The low-level transport. Must provide the following methods: + * + * launch(url): Opens the url in the target browser. + * send(idOrArray, string): Dispatches the given protocol message (provided as a JSON string) to the given client ID + * or array of client IDs. (See the "connect" message for an explanation of client IDs.) + * close(id): Closes the connection to the given client ID. + * getRemoteScript(): Returns a script that should be injected into the page's HTML in order to handle the remote side + * of the transport. Should include the "\n"; + } + + /** + * Returns a script that should be injected into the HTML that's launched in the + * browser in order to handle protocol requests. Includes the \n" + + remoteFunctionsScript; + + } + + /** + * Launches the given URL in the browser. Proxies to the transport. + * @param {string} url + */ + function launch(url) { + _transport.launch(url); + } + + /** + * Protocol method. Evaluates the given script in the browser (in global context), and returns a promise + * that will be fulfilled with the result of the script, if any. + * @param {number|Array.} clients A client ID or array of client IDs that should evaluate + * the script. + * @param {string} script The script to evalute. + * @return {$.Promise} A promise that's resolved with the return value from the first client that responds + * to the evaluation. + */ + function evaluate(script, clients) { + return _send( + { + method: "Runtime.evaluate", + params: { + expression: script + } + }, + clients + ); + } + + /** + * Protocol method. Reloads the page that is currently loaded into the browser, optionally ignoring cache. + * @param {number|Array.} clients A client ID or array of client IDs that should reload the page. + * @param {boolean} ignoreCache If true, browser cache is ignored. + * @return {$.Promise} A promise that's resolved with the return value from the first client that responds + * to the method. + */ + function reload(ignoreCache, clients) { + return _send( + { + method: "Page.reload", + params: { + ignoreCache: true + } + }, + clients + ); + } + + /** + * Protocol method. Navigates current page to the given URL. + * @param {number|Array.} clients A client ID or array of client IDs that should navigate to the given URL. + * @param {string} url URL to navigate the page to. + * @return {$.Promise} A promise that's resolved with the return value from the first client that responds + * to the method. + */ + function navigate(url, clients) { + return _send( + { + method: "Page.navigate", + params: { + url: url + } + }, + clients + ); + } + + /** + * Closes the connection to the given client. Proxies to the transport. + * @param {number} clientId + */ + function close(clientId) { + _transport.close(clientId); + } + + function closeAllConnections() { + getConnectionIds().forEach(function (clientId) { + close(clientId); + }); + _connections = {}; + } + + // public API + exports.setTransport = setTransport; + exports.getRemoteScript = getRemoteScript; + exports.launch = launch; + exports.evaluate = evaluate; + exports.reload = reload; + exports.navigate = navigate; + exports.close = close; + exports.getConnectionIds = getConnectionIds; + exports.closeAllConnections = closeAllConnections; +}); diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js new file mode 100644 index 00000000000..254c6e3d7e6 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2014 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. + * + */ + +(function (global) { + "use strict"; + + var ProtocolManager = global._Brackets_LiveDev_ProtocolManager; + + var _document = null; + var _transport; + + + /** + * Common functions. + */ + var Utils = { + + isExternalStylesheet: function (node) { + return (node.nodeName.toUpperCase() === "LINK" && node.rel === "stylesheet" && node.href); + }, + isExternalScript: function (node) { + return (node.nodeName.toUpperCase() === "SCRIPT" && node.src); + } + }; + + /** + * CSS related commands and notifications + */ + var CSS = { + + /** + * Maintains a map of stylesheets loaded thorugh @import rules and their parents. + * Populated by extractImports, consumed by notifyImportsAdded / notifyImportsRemoved. + * @type { + */ + stylesheets : {}, + + /** + * Check the stylesheet that was just added be really loaded + * to be able to extract potential import-ed stylesheets. + * It invokes notifyStylesheetAdded once the sheet is loaded. + * @param {string} href Absolute URL of the stylesheet. + */ + checkForStylesheetLoaded : function (href) { + var self = this; + + + // Inspect CSSRules for @imports: + // styleSheet obejct is required to scan CSSImportRules but + // browsers differ on the implementation of MutationObserver interface. + // Webkit triggers notifications before stylesheets are loaded, + // Firefox does it after loading. + // There are also differences on when 'load' event is triggered for + // the 'link' nodes. Webkit triggers it before stylesheet is loaded. + // Some references to check: + // http://www.phpied.com/when-is-a-stylesheet-really-loaded/ + // http://stackoverflow.com/questions/17747616/webkit-dynamically-created-stylesheet-when-does-it-really-load + // http://stackoverflow.com/questions/11425209/are-dom-mutation-observers-slower-than-dom-mutation-events + // + // TODO: This is just a temporary 'cross-browser' solution, it needs optimization. + var loadInterval = setInterval(function () { + var i; + for (i = 0; i < document.styleSheets.length; i++) { + if (document.styleSheets[i].href === href) { + //clear interval + clearInterval(loadInterval); + // notify stylesheets added + self.notifyStylesheetAdded(href); + break; + } + } + }, 50); + }, + /** + * Send a notification for the stylesheet added and + * its import-ed styleshets based on document.stylesheets diff + * from previous status. It also updates stylesheets status. + */ + notifyStylesheetAdded : function () { + var i, + added = {}, + current, + newStatus; + + current = this.stylesheets; + newStatus = related().stylesheets; + + Object.keys(newStatus).forEach(function (v, i) { + if (!current[v]) { + added[v] = newStatus[v]; + } + }); + + Object.keys(added).forEach(function (v, i) { + _transport.send(JSON.stringify({ + method: "Stylesheet.Added", + href: v, + roots: added[v] + })); + }); + + this.stylesheets = newStatus; + }, + + /** + * Send a notification for the removed stylesheet and + * its import-ed styleshets based on document.stylesheets diff + * from previous status. It also updates stylesheets status. + */ + notifyStylesheetRemoved : function () { + var i, + removed = {}, + newStatus, + current; + + current = this.stylesheets; + newStatus = related().stylesheets; + + Object.keys(current).forEach(function (v, i) { + if (!newStatus[v]) { + removed[v] = current[v]; + } + }); + + Object.keys(removed).forEach(function (v, i) { + _transport.send(JSON.stringify({ + method: "Stylesheet.Removed", + href: v, + roots: removed[v] + })); + }); + + this.stylesheets = newStatus; + } + }; + + + /* process related docs added */ + function _onNodesAdded(nodes) { + var i; + for (i = 0; i < nodes.length; i++) { + //check for Javascript files + if (Utils.isExternalScript(nodes[i])) { + _transport.send(JSON.stringify({ + method: 'Script.Added', + src: nodes[i].src + })); + } + //check for stylesheets + if (Utils.isExternalStylesheet(nodes[i])) { + CSS.checkForStylesheetLoaded(nodes[i].href); + } + } + } + /* process related docs removed */ + function _onNodesRemoved(nodes) { + var i; + //iterate on removed nodes + for (i = 0; i < nodes.length; i++) { + + // check for external JS files + if (Utils.isExternalScript(nodes[i])) { + _transport.send(JSON.stringify({ + method: 'Script.Removed', + src: nodes[i].src + })); + } + //check for external StyleSheets + if (Utils.isExternalStylesheet(nodes[i])) { + CSS.notifyStylesheetRemoved(nodes[i].href); + } + } + } + + function _enableListeners() { + // enable MutationOberver if it's supported + var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; + if (MutationObserver) { + var observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.addedNodes.length > 0) { + _onNodesAdded(mutation.addedNodes); + } + if (mutation.removedNodes.length > 0) { + _onNodesRemoved(mutation.removedNodes); + } + }); + }); + observer.observe(_document, { + childList: true, + subtree: true + }); + } else { + // use MutationEvents as fallback + document.addEventListener('DOMNodeInserted', function niLstnr(e) { + _onNodesAdded([e.target]); + }); + document.addEventListener('DOMNodeRemoved', function nrLstnr(e) { + _onNodesRemoved([e.target]); + }); + } + } + + /** + * Retrieves related documents (external CSS and JS files) + * + * @return {{scripts: object, stylesheets: object}} Related scripts and stylesheets + */ + function related() { + + var rel = { + scripts: {}, + stylesheets: {} + }; + var i; + // iterate on document scripts (HTMLCollection doesn't provide forEach iterator). + for (i = 0; i < _document.scripts.length; i++) { + // add only external scripts + if (_document.scripts[i].src) { + rel.scripts[_document.scripts[i].src] = true; + } + } + + var s, j; + //traverse @import rules + var traverseRules = function _traverseRules(sheet, base) { + var i; + if (sheet.href && sheet.cssRules) { + if (rel.stylesheets[sheet.href] === undefined) { + rel.stylesheets[sheet.href] = []; + } + rel.stylesheets[sheet.href].push(base); + + + for (i = 0; i < sheet.cssRules.length; i++) { + if (sheet.cssRules[i].href) { + traverseRules(sheet.cssRules[i].styleSheet, base); + } + } + } + }; + //iterate on document.stylesheets (StyleSheetList doesn't provide forEach iterator). + for (j = 0; j < document.styleSheets.length; j++) { + s = document.styleSheets[j]; + traverseRules(s, s.href); + } + return rel; + } + + /** + * Start listening for events and send initial related documents message. + * + * @param {HTMLDocument} document + * @param {object} transport Live development transport connection + */ + function start(document, transport) { + _transport = transport; + _document = document; + // start listening to node changes + _enableListeners(); + + var rel = related(); + + // send the current status of related docs. + _transport.send(JSON.stringify({ + method: "Document.Related", + related: rel + })); + // initialize stylesheets with current status for further notifications. + CSS.stylesheets = rel.stylesheets; + } + + /** + * Stop listening. + * TODO currently a no-op. + */ + function stop() { + + } + + var DocumentObserver = { + start: start, + stop: stop, + related: related + }; + + ProtocolManager.setDocumentObserver(DocumentObserver); + +}(this)); \ No newline at end of file diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js new file mode 100644 index 00000000000..8ca1671595b --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014 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, browser: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ +/*global define, $, window, navigator, Node, console */ + +/** + * ExtendRemoteFunctions defines the addtional functions to be executed in the browser. + */ +function ExtendRemoteFunctions(obj) { + "use strict"; + + var ExtendedObj = function () {}; + ExtendedObj.prototype = obj; + + ExtendedObj.prototype.reloadCSS = function reloadCSS(url, text) { + var i, + node; + + var head = document.getElementsByTagName('head')[0]; + // create an style element to replace the one loaded with + var s = document.createElement('style'); + s.type = 'text/css'; + s.appendChild(document.createTextNode(text)); + + for (i = 0; i < document.styleSheets.length; i++) { + node = document.styleSheets[i]; + if (node.ownerNode.id === url) { + head.insertBefore(s, node.ownerNode); // insert the style element here + // now can remove the style element previously created (if any) + node.ownerNode.parentNode.removeChild(node.ownerNode); + } else if (node.href === url && !node.disabled) { + // if the link element to change + head.insertBefore(s, node.ownerNode); // insert the style element here + node.disabled = true; + i++; // since we have just inserted a stylesheet + } + } + s.id = url; + }; + return new ExtendedObj(); +} \ No newline at end of file diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js new file mode 100644 index 00000000000..b7a5e08fae1 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2014 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 evil: true */ + +// This is the script that Brackets live development injects into HTML pages in order to +// establish and maintain the live development socket connection. Note that Brackets may +// also inject other scripts via "evaluate" once this has connected back to Brackets. + +(function (global) { + "use strict"; + + // This protocol handler assumes that there is also an injected transport script that + // has the following methods: + // setCallbacks(obj) - a method that takes an object with a "message" callback that + // will be called with the message string whenever a message is received by the transport. + // send(msgStr) - sends the given message string over the transport. + var transport = global._Brackets_LiveDev_Transport; + + + /** + * Manage messaging between Editor and Browser at the protocol layer. + * Handle messages that arrives through the current transport and dispatch them + * to subscribers. Subscribers are handlers that implements remote commands/functions. + * Property 'method' of messages body is used as the 'key' to identify message types. + * Provide a 'send' operation that allows remote commands sending messages to the Editor. + */ + var MessageBroker = { + + /** + * Collection of handlers (subscribers) per each method. + * To be pushed by 'on' and consumed by 'trigger' stored this way: + * handlers[method] = [handler1, handler2, ...] + */ + handlers: {}, + + /** + * Dispatch messages to handlers according to msg.method value. + * @param {Object} msg Message to be dispatched. + */ + trigger: function (msg) { + var msgHandlers; + if (!msg.method) { + // no message type, ignoring it + // TODO: should we trigger a generic event? + console.log("[Brackets LiveDev] Received message without method."); + return; + } + // get handlers for msg.method + msgHandlers = this.handlers[msg.method]; + + if (msgHandlers && msgHandlers.length > 0) { + // invoke handlers with the received message + msgHandlers.forEach(function (handler) { + try { + // TODO: check which context should be used to call handlers here. + handler(msg); + return; + } catch (e) { + console.log("[Brackets LiveDev] Error executing a handler for " + msg.method); + console.log(e.stack); + return; + } + }); + } else { + // no subscribers, ignore it. + // TODO: any other default handling? (eg. specific respond, trigger as a generic event, etc.); + console.log("[Brackets LiveDev] No subscribers for message " + msg.method); + return; + } + }, + + /** + * Send a response of a particular message to the Editor. + * Original message must provide an 'id' property + * @param {Object} orig Original message. + * @param {Object} response Message to be sent as the response. + */ + respond: function (orig, response) { + if (!orig.id) { + console.log("[Brackets LiveDev] Trying to send a response for a message with no ID"); + return; + } + response.id = orig.id; + this.send(JSON.stringify(response)); + }, + + /** + * Subscribe handlers to specific messages. + * @param {string} method Message type. + * @param {function} handler. + * TODO: add handler name or any identification mechanism to then implement 'off'? + */ + on: function (method, handler) { + if (!method || !handler) { + return; + } + if (!this.handlers[method]) { + //initialize array + this.handlers[method] = []; + } + // add handler to the stack + this.handlers[method].push(handler); + }, + + /** + * Send a message to the Editor. + * @param {string} msgStr Message to be sent. + */ + send: function (msgStr) { + transport.send(JSON.stringify(msgStr)); + } + }; + + /** + * Runtime Domain. Implements remote commands for "Runtime.*" + */ + var Runtime = { + /** + * Evaluate an expresion and return its result. + */ + evaluate: function (msg) { + console.log("Runtime.evaluate"); + var result = eval(msg.params.expression); + MessageBroker.respond(msg, { + result: JSON.stringify(result) // TODO: in original protocol this is an object handle + }); + } + }; + + // subscribe handler to method Runtime.evaluate + MessageBroker.on("Runtime.evaluate", Runtime.evaluate); + + /** + * Page Domain. + */ + var Page = { + /** + * Reload the current page optionally ignoring cache. + * @param {Object} msg + */ + reload: function (msg) { + // just reload the page + window.location.reload(msg.params.ignoreCache); + }, + + /** + * Navigate to a different page. + * @param {Object} msg + */ + navigate: function (msg) { + if (msg.params.url) { + // navigate to a new page. + window.location.replace(msg.params.url); + } + } + }; + + // subscribe handler to method Page.reload + MessageBroker.on("Page.reload", Page.reload); + MessageBroker.on("Page.navigate", Page.navigate); + MessageBroker.on("Connection.close", Page.close); + + + + // By the time this executes, there must already be an active transport. + if (!transport) { + console.error("[Brackets LiveDev] No transport set"); + return; + } + + var ProtocolManager = { + + _documentObserver: {}, + + _protocolHandler: {}, + + enable: function () { + transport.setCallbacks(this._protocolHandler); + transport.enable(); + }, + + onConnect: function () { + this._documentObserver.start(window.document, transport); + }, + + onClose: function () { + // TODO: This is absolutely temporary solution. It shows a message + // when the connection has been closed. UX decision to be taken on what to do when + // the session is explicitly closed from the Editor side. If the browser can't be closed, + // this could be an alternative. A better alternative to this could be a redirection + // to a custom static page being served by StaticServer + var body = document.getElementsByTagName("body")[0]; + body.style.opacity = 0.5; + var status = document.createElement("div"); + status.textContent = "Live Development Session has Ended"; + status.style.width = "100%"; + status.style.color = "#fff"; + status.style.backgroundColor = "#ff0000"; + status.style.position = "absolute"; + status.style.top = 0; + status.style.left = 0; + status.style.padding = "0.2em"; + status.style.zIndex = 2227; + body.appendChild(status); + }, + + setDocumentObserver: function (documentOberver) { + if (!documentOberver) { + return; + } + this._documentObserver = documentOberver; + }, + + setProtocolHandler: function (protocolHandler) { + if (!protocolHandler) { + return; + } + this._protocolHandler = protocolHandler; + } + }; + + // exposing ProtocolManager + global._Brackets_LiveDev_ProtocolManager = ProtocolManager; + + /** + * The remote handler for the protocol. + */ + var ProtocolHandler = { + /** + * Handles a message from the transport. Parses it as JSON and delegates + * to MessageBroker who is in charge of routing them to handlers. + * @param {string} msgStr The protocol message as stringified JSON. + */ + message: function (msgStr) { + var msg; + try { + msg = JSON.parse(msgStr); + } catch (e) { + console.log("[Brackets LiveDev] Invalid Message Received"); + // TODO: we should probably send back an error message here? + return; + } + // delegates handling/routing to MessageBroker. + MessageBroker.trigger(msg); + }, + + close: function (evt) { + ProtocolManager.onClose(); + }, + + connect: function (evt) { + ProtocolManager.onConnect(); + } + }; + + ProtocolManager.setProtocolHandler(ProtocolHandler); + + window.addEventListener('load', function () { + ProtocolManager.enable(); + }); + +}(this)); diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js b/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js new file mode 100644 index 00000000000..c4d6e39ca96 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ +/*global define, $, brackets, window, open */ + +// This transport provides a WebSocket connection between Brackets and a live browser preview. +// This is just a thin wrapper around the Node extension (NodeSocketTransportDomain) that actually +// provides the WebSocket server and handles the communication. We also rely on an injected script in +// the browser for the other end of the transport. + +define(function (require, exports, module) { + "use strict"; + + var FileUtils = require("file/FileUtils"), + NodeDomain = require("utils/NodeDomain"); + + // The script that will be injected into the previewed HTML to handle the other side of the socket connection. + var NodeSocketTransportRemote = require("text!LiveDevelopment/MultiBrowserImpl/transports/remote/NodeSocketTransportRemote.js"); + + // The node extension that actually provides the WebSocket server. + + var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/NodeSocketTransportDomain"; + + var NodeSocketTransportDomain = new NodeDomain("nodeSocketTransport", domainPath); + + // This must match the port declared in NodeSocketTransportDomain.js. + // TODO: randomize this? + var SOCKET_PORT = 8123; + + /** + * Returns the script that should be injected into the browser to handle the other end of the transport. + * @return {string} + */ + function getRemoteScript() { + return "\n"; + } + + // Events + + // We can simply retrigger the events we receive from the node domain directly, since they're in + // the same format expected by clients of the transport. + ["connect", "message", "close"].forEach(function (type) { + $(NodeSocketTransportDomain).on(type, function () { + console.log("NodeSocketTransport - event - " + type + " - " + JSON.stringify(Array.prototype.slice.call(arguments, 1))); + // Remove the event object from the argument list. + $(exports).triggerHandler(type, Array.prototype.slice.call(arguments, 1)); + }); + }); + + // Exports + + exports.getRemoteScript = getRemoteScript; + + // Proxy the node domain methods directly through, since they have exactly the same + // signatures as the ones we're supposed to provide. + ["launch", "send", "close"].forEach(function (method) { + exports[method] = function () { + var args = Array.prototype.slice.call(arguments); + args.unshift(method); + console.log("NodeSocketTransport - " + args); + NodeSocketTransportDomain.exec.apply(NodeSocketTransportDomain, args); + }; + }); + +}); diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js b/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js new file mode 100644 index 00000000000..33c22c78cba --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, node: true */ + +(function () { + "use strict"; + + var WebSocketServer = require("ws").Server, + open = require("open"), + _ = require("lodash"); + + /** + * @private + * The WebSocket server we listen for incoming connections on. + * @type {?WebSocketServer} + */ + var _wsServer; + + /** + * @private + * The Brackets domain manager for registering node extensions. + * @type {?DomainManager} + */ + var _domainManager; + + /** + * @private + * The ID that should be allocated to the next client that connects to the transport. + * @type {number} + */ + var _nextClientId = 1; + + /** + * @private + * A map of client IDs to the URL and WebSocket for the given ID. + * @type {Object.} + */ + var _clients = {}; + + // This must match the port declared in NodeSocketTransport.js. + // TODO: randomize this? + var SOCKET_PORT = 8123; + + /** + * @private + * Returns the client info for a given WebSocket, or null if that socket isn't registered. + * @param {WebSocket} ws + * @return {?{id: number, url: string, socket: WebSocket}} + */ + function _clientForSocket(ws) { + return _.find(_clients, function (client) { + return (client.socket === ws); + }); + } + + /** + * @private + * Creates the WebSocketServer and handles incoming connections. + */ + function _createServer() { + if (!_wsServer) { + // TODO: make port configurable, or use random port + _wsServer = new WebSocketServer({port: SOCKET_PORT}); + _wsServer.on("connection", function (ws) { + ws.on("message", function (msg) { + console.log("WebSocketServer - received - " + msg); + var msgObj; + try { + msgObj = JSON.parse(msg); + } catch (e) { + console.error("nodeSocketTransport: Error parsing message: " + msg); + return; + } + + // See the comment in NodeSocketTransportRemote.connect() for why we have an extra + // layer of transport-layer message objects surrounding the protocol messaging. + + if (msgObj.type === "connect") { + if (!msgObj.url) { + console.error("nodeSocketTransport: Malformed connect message: " + msg); + return; + } + var clientId = _nextClientId++; + _clients[clientId] = { + id: clientId, + url: msgObj.url, + socket: ws + }; + console.log("emitting connect event"); + _domainManager.emitEvent("nodeSocketTransport", "connect", [clientId, msgObj.url]); + } else if (msgObj.type === "message") { + var client = _clientForSocket(ws); + if (client) { + _domainManager.emitEvent("nodeSocketTransport", "message", [client.id, msgObj.message]); + } else { + console.error("nodeSocketTransport: Couldn't locate client for message: " + msg); + } + } else { + console.error("nodeSocketTransport: Got bad socket message type: " + msg); + } + }).on("error", function (e) { + // TODO: emit error event + var client = _clientForSocket(ws); + console.error("nodeSocketTransport: Error on socket for client " + JSON.stringify(client) + ": " + e); + }).on("close", function () { + var client = _clientForSocket(ws); + if (client) { + _domainManager.emitEvent("nodeSocketTransport", "close", [client.id]); + delete _clients[client.id]; + } else { + console.error("nodeSocketTransport: Socket closed, but couldn't locate client"); + } + }); + }); + } + } + + /** + * Initializes the socket server, then launches the given URL in the system default browser. + * @param {string} url + */ + function _cmdLaunch(url) { + _createServer(); + open(url); + } + + /** + * Sends a transport-layer message over the socket. + * @param {number|Array.} idOrArray A client ID or array of client IDs to send the message to. + * @param {string} msgStr The message to send as a JSON string. + */ + function _cmdSend(idOrArray, msgStr) { + if (!Array.isArray(idOrArray)) { + idOrArray = [idOrArray]; + } + idOrArray.forEach(function (id) { + var client = _clients[id]; + if (!client) { + console.error("nodeSocketTransport: Couldn't find client ID: " + id); + } else { + client.socket.send(msgStr); + } + }); + } + + /** + * Closes the connection for a given client ID. + * @param {number} clientId + */ + function _cmdClose(clientId) { + var client = _clients[clientId]; + if (client) { + client.socket.close(); + delete _clients[clientId]; + } + } + + /** + * Initializes the domain and registers commands. + * @param {DomainManager} domainManager The DomainManager for the server + */ + function init(domainManager) { + _domainManager = domainManager; + if (!domainManager.hasDomain("nodeSocketTransport")) { + domainManager.registerDomain("nodeSocketTransport", {major: 0, minor: 1}); + } + domainManager.registerCommand( + "nodeSocketTransport", // domain name + "launch", // command name + _cmdLaunch, // command handler function + false, // this command is synchronous in Node + "Launches a given HTML file in the browser for live development", + [{name: "url", // parameters + type: "string", + description: "file:// url to the HTML file"}], + [] + ); + domainManager.registerCommand( + "nodeSocketTransport", // domain name + "send", // command name + _cmdSend, // command handler function + false, // this command is synchronous in Node + "Sends a message to a given client or list of clients", + [ + {name: "idOrArray", type: "number|Array.", description: "id or array of ids to send the message to"}, + {name: "message", type: "string", description: "JSON message to send"} + ], + [] + ); + domainManager.registerCommand( + "nodeSocketTransport", // domain name + "close", // command name + _cmdClose, // command handler function + false, // this command is synchronous in Node + "Closes the connection to a given client", + [ + {name: "id", type: "number", description: "id of connection to close"} + ], + [] + ); + domainManager.registerEvent( + "nodeSocketTransport", + "connect", + [ + {name: "clientID", type: "number", description: "ID of live preview page connecting to live development"}, + {name: "url", type: "string", description: "URL of page that live preview is connecting from"} + ] + ); + domainManager.registerEvent( + "nodeSocketTransport", + "message", + [ + {name: "clientID", type: "number", description: "ID of live preview page sending message"}, + {name: "msg", type: "string", description: "JSON message from client page"} + ] + ); + domainManager.registerEvent( + "nodeSocketTransport", + "close", + [ + {name: "clientID", type: "number", description: "ID of live preview page being closed"} + ] + ); + } + + exports.init = init; + +}()); diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json b/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json new file mode 100644 index 00000000000..17e04631800 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json @@ -0,0 +1,8 @@ +{ + "name": "brackets-livedev2-server", + "dependencies": { + "ws": "~0.4.31", + "open": "0.0.4", + "lodash": "~2.4.1" + } +} diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/remote/NodeSocketTransportRemote.js b/src/LiveDevelopment/MultiBrowserImpl/transports/remote/NodeSocketTransportRemote.js new file mode 100644 index 00000000000..02b2dacad50 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/remote/NodeSocketTransportRemote.js @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2014 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 browser: true, vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ +/*global WebSocket */ + +// This is a transport injected into the browser via a script that handles the low +// level communication between the live development protocol handlers on both sides. +// This transport provides a web socket mechanism. It's injected separately from the +// protocol handler so that the transport can be changed separately. + +(function (global) { + "use strict"; + + var WebSocketTransport = { + /** + * @private + * The WebSocket that we communicate with Brackets over. + * @type {?WebSocket} + */ + _ws: null, + + /** + * @private + * An object that contains callbacks to handle various transport events. See `setCallbacks()`. + * @type {?{connect: ?function, message: ?function(string), close: ?function}} + */ + _callbacks: null, + + /** + * Sets the callbacks that should be called when various transport events occur. All callbacks + * are optional, but you should at least implement "message" or nothing interesting will happen :) + * @param {?{connect: ?function, message: ?function(string), close: ?function}} callbacks + * The callbacks to set. + * connect - called when a connection is established to Brackets + * message(msgStr) - called with a string message sent from Brackets + * close - called when Brackets closes the connection + */ + setCallbacks: function (callbacks) { + if (!global._Brackets_LiveDev_Socket_Transport_URL) { + console.error("[Brackets LiveDev] No socket transport URL injected"); + } else { + this._callbacks = callbacks; + } + }, + + /** + * Connects to the NodeSocketTransport in Brackets at the given WebSocket URL. + * @param {string} url + */ + connect: function (url) { + var self = this; + this._ws = new WebSocket(url); + + // One potential source of confusion: the transport sends two "types" of messages - + // these are distinct from the protocol's own messages. This is because this transport + // needs to send an initial "connect" message telling the Brackets side of the transport + // the URL of the page that it's connecting from, distinct from the actual protocol + // message traffic. Actual protocol messages are sent as a JSON payload in a message of + // type "message". + // + // Other transports might not need to do this - for example, a transport that simply + // talks to an iframe within the same process already knows what URL that iframe is + // pointing to, so the only comunication that needs to happen via postMessage() is the + // actual protocol message strings, and no extra wrapping is necessary. + + this._ws.onopen = function (event) { + // Send the initial "connect" message to tell the other end what URL we're from. + self._ws.send(JSON.stringify({ + type: "connect", + url: global.location.href + })); + console.log("[Brackets LiveDev] Connected to Brackets at " + url); + if (self._callbacks && self._callbacks.connect) { + self._callbacks.connect(); + } + }; + this._ws.onmessage = function (event) { + console.log("[Brackets LiveDev] Got message: " + event.data); + if (self._callbacks && self._callbacks.message) { + self._callbacks.message(event.data); + } + }; + this._ws.onclose = function (event) { + self._ws = null; + if (self._callbacks && self._callbacks.close) { + self._callbacks.close(); + } + }; + // TODO: onerror + }, + + /** + * Sends a message over the transport. + * @param {string} msgStr The message to send. + */ + send: function (msgStr) { + if (this._ws) { + // See comment in `connect()` above about why we wrap the message in a transport message + // object. + this._ws.send(JSON.stringify({ + type: "message", + message: msgStr + })); + } else { + console.log("[Brackets LiveDev] Tried to send message over closed connection: " + msgStr); + } + }, + + /** + * Establish web socket connection. + */ + enable: function() { + this.connect(global._Brackets_LiveDev_Socket_Transport_URL); + } + }; + global._Brackets_LiveDev_Transport = WebSocketTransport; +}(this)); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 0c8c7f68eb0..19590df92a3 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -49,10 +49,14 @@ define(function main(require, exports, module) { Strings = require("strings"), ExtensionUtils = require("utils/ExtensionUtils"), StringUtils = require("utils/StringUtils"); + + // expermiental multi-browser implementation + var MultiBrowserLiveDev = require("LiveDevelopment/LiveDevMultiBrowser"); var params = new UrlParams(); var config = { experimental: false, // enable experimental features + multiBrowser: false, // enable experimental multi-browser implementation debug: true, // enable debug output and helpers autoconnect: false, // go live automatically after startup? highlight: true, // enable highlighting? @@ -233,10 +237,19 @@ define(function main(require, exports, module) { /** Initialize LiveDevelopment */ AppInit.appReady(function () { + params.parse(); - - Inspector.init(config); - LiveDevelopment.init(config); + + if (!config.multiBrowser) { + // init LiveDevelopment + Inspector.init(config); + LiveDevelopment.init(config); + } else { + // init experimental multi-browser implementation + LiveDevelopment = MultiBrowserLiveDev; + LiveDevelopment.init(config); + } + _loadStyles(); _setupGoLiveButton(); _setupGoLiveMenu(); From 3b96b2df80b762d3cd1694b2c526d61682819d77 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Tue, 18 Nov 2014 16:04:27 -0300 Subject: [PATCH 02/17] remove main.js from MultiBrowserImpl --- src/LiveDevelopment/MultiBrowserImpl/main.js | 214 ------------------- 1 file changed, 214 deletions(-) delete mode 100644 src/LiveDevelopment/MultiBrowserImpl/main.js diff --git a/src/LiveDevelopment/MultiBrowserImpl/main.js b/src/LiveDevelopment/MultiBrowserImpl/main.js deleted file mode 100644 index 91299f28827..00000000000 --- a/src/LiveDevelopment/MultiBrowserImpl/main.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - - -/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ -/*global brackets, define, $, less, window */ - -/** - * main integrates LiveDevelopment into Brackets - * - * This module creates two menu items: - * - * "Go Live": open or close a Live Development session and visualize the status - * "Highlight": toggle source highlighting - * - * @require DocumentManager - */ -define(function main(require, exports, module) { - "use strict"; - - var DocumentManager = require("document/DocumentManager"), - Commands = require("command/Commands"), - AppInit = require("utils/AppInit"), - CommandManager = require("command/CommandManager"), - PreferencesManager = require("preferences/PreferencesManager"), - Dialogs = require("widgets/Dialogs"), - DefaultDialogs = require("widgets/DefaultDialogs"), - UrlParams = require("utils/UrlParams").UrlParams, - Strings = require("strings"), - ExtensionUtils = require("utils/ExtensionUtils"), - StringUtils = require("utils/StringUtils"), - Menus = require("command/Menus"), - LiveDevelopment = require("LiveDevelopment/MultiBrowserImpl/LiveDevelopment"); - - var params = new UrlParams(); - - // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. - var _statusTooltip = [ - Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, - Strings.LIVE_DEV_STATUS_TIP_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, - Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR, - Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, - Strings.LIVE_DEV_STATUS_TIP_PROGRESS1 - ]; - - var _statusStyle = ["warning", "", "info", "success", "out-of-sync", "sync-error", "info", "info"]; // Status indicator's CSS class - var _allStatusStyles = _statusStyle.join(" "); - - var _$btnGoLive; // reference to the GoLive button - var _$btnHighlight; // reference to the HighlightButton - - /** - * Change the appearance of a button. Omit text to remove any extra text; omit style to return to default styling; - * omit tooltip to leave tooltip unchanged. - */ - function _setLabel($btn, text, style, tooltip) { - // Clear text/styles from previous status - $("span", $btn).remove(); - $btn.removeClass(_allStatusStyles); - - // Set text/styles for new status - if (text && text.length > 0) { - $("") - .addClass(style) - .text(text) - .appendTo($btn); - } else { - $btn.addClass(style); - } - - if (tooltip) { - $btn.attr("title", tooltip); - } - } - - /** - * Toggles LiveDevelopment and synchronizes the state of UI elements that reports LiveDevelopment status - * - * Stop Live Dev when in an active state (ACTIVE, OUT_OF_SYNC, SYNC_ERROR). - * Start Live Dev when in an inactive state (ERROR, INACTIVE). - * Do nothing when in a connecting state (CONNECTING, LOADING_AGENTS). - */ - function _handleGoLiveCommand() { - if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { - LiveDevelopment.close(); - } else if (LiveDevelopment.status <= LiveDevelopment.STATUS_INACTIVE) { - if (!params.get("skipLiveDevelopmentInfo") && !PreferencesManager.getViewState("livedev2.afterFirstLaunch")) { - PreferencesManager.setViewState("livedev2.afterFirstLaunch", "true"); - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - Strings.LIVE_DEVELOPMENT_INFO_TITLE, - Strings.LIVE_DEVELOPMENT_INFO_MESSAGE - ).done(function (id) { - LiveDevelopment.open(); - }); - } else { - LiveDevelopment.open(); - } - } - } - - /** Called on status change */ - function _showStatusChangeReason(reason) { - // Destroy the previous twipsy (options are not updated otherwise) - _$btnGoLive.twipsy("hide").removeData("twipsy"); - - // If there was no reason or the action was an explicit request by the user, don't show a twipsy - if (!reason || reason === "explicit_close") { - return; - } - - // Translate the reason - var translatedReason = Strings["LIVE_DEV_" + reason.toUpperCase()]; - if (!translatedReason) { - translatedReason = StringUtils.format(Strings.LIVE_DEV_CLOSED_UNKNOWN_REASON, reason); - } - - // Configure the twipsy - var options = { - placement: "left", - trigger: "manual", - autoHideDelay: 5000, - title: function () { - return translatedReason; - } - }; - - // Show the twipsy with the explanation - _$btnGoLive.twipsy(options).twipsy("show"); - } - - /** Create the menu item "Go Live" */ - function _setupGoLiveButton() { - _$btnGoLive = _$btnGoLive = $("#toolbar-go-live"); - _$btnGoLive.click(function onGoLive() { - _handleGoLiveCommand(); - }); - $(LiveDevelopment).on("statusChange", function statusChange(event, status, reason) { - // status starts at -1 (error), so add one when looking up name and style - // See the comments at the top of LiveDevelopment.js for details on the - // various status codes. - _setLabel(_$btnGoLive, null, _statusStyle[status + 1], _statusTooltip[status + 1]); - _showStatusChangeReason(reason); - }); - - // Initialize tooltip for 'not connected' state - _setLabel(_$btnGoLive, null, _statusStyle[1], _statusTooltip[1]); - } - - /** Maintains state of the Live Preview menu item */ - function _setupGoLiveMenu() { - $(LiveDevelopment).on("statusChange", function statusChange(event, status) { - // Update the checkmark next to 'Live Preview' menu item - // Add checkmark when status is STATUS_ACTIVE; otherwise remove it - CommandManager.get("file.previewHighlight").setEnabled(status === LiveDevelopment.STATUS_INACTIVE); - }); - } - - function _updateHighlightCheckmark() { - CommandManager.get("file.previewHighlight").setChecked(PreferencesManager.getViewState("file.previewHighlight")); - } - - function _handlePreviewHighlightCommand() { - PreferencesManager.setViewState("file.previewHighlight", !PreferencesManager.getViewState("file.previewHighlight")); - } - - /** Initialize LiveDevelopment */ - function init() { - params.parse(); - - LiveDevelopment.init(); - _setupGoLiveButton(); - _setupGoLiveMenu(); - - _updateHighlightCheckmark(); - } - - // init prefs - PreferencesManager.stateManager.definePreference("file.previewHighlight", "boolean", true) - .on("change", function () { - _updateHighlightCheckmark(); - }); - - // init commands - CommandManager.register(Strings.CMD_LIVE_FILE_PREVIEW, Commands.FILE_LIVE_FILE_PREVIEW, _handleGoLiveCommand); - CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, _handlePreviewHighlightCommand); - CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); - - // Export public functions - exports.init = init; -}); From 488296d3539f749bdb3a75810c243b05c9fe433a Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Wed, 19 Nov 2014 17:27:01 -0300 Subject: [PATCH 03/17] add switch based on pref livedev.multibrowser It now allows switching btw implementations by setting a preference. livedev.multibrowser set to true enables MultiBrowserImpl. --- src/LiveDevelopment/main.js | 69 ++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 19590df92a3..44bd0a89666 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -56,7 +56,6 @@ define(function main(require, exports, module) { var params = new UrlParams(); var config = { experimental: false, // enable experimental features - multiBrowser: false, // enable experimental multi-browser implementation debug: true, // enable debug output and helpers autoconnect: false, // go live automatically after startup? highlight: true, // enable highlighting? @@ -83,6 +82,8 @@ define(function main(require, exports, module) { var _allStatusStyles = _statusStyle.join(" "); var _$btnGoLive; // reference to the GoLive button + + var LiveDevImpl; /** Load Live Development LESS Style */ function _loadStyles() { @@ -127,9 +128,10 @@ define(function main(require, exports, module) { * Do nothing when in a connecting state (CONNECTING, LOADING_AGENTS). */ function _handleGoLiveCommand() { - if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { - LiveDevelopment.close(); - } else if (LiveDevelopment.status <= LiveDevelopment.STATUS_INACTIVE) { + + if (LiveDevImpl.status >= LiveDevImpl.STATUS_ACTIVE) { + LiveDevImpl.close(); + } else if (LiveDevImpl.status <= LiveDevImpl.STATUS_INACTIVE) { if (!params.get("skipLiveDevelopmentInfo") && !PreferencesManager.getViewState("livedev.afterFirstLaunch")) { PreferencesManager.setViewState("livedev.afterFirstLaunch", "true"); Dialogs.showModalDialog( @@ -137,10 +139,10 @@ define(function main(require, exports, module) { Strings.LIVE_DEVELOPMENT_INFO_TITLE, Strings.LIVE_DEVELOPMENT_INFO_MESSAGE ).done(function (id) { - LiveDevelopment.open(); + LiveDevImpl.open(); }); } else { - LiveDevelopment.open(); + LiveDevImpl.open(); } } } @@ -181,7 +183,7 @@ define(function main(require, exports, module) { _$btnGoLive.click(function onGoLive() { _handleGoLiveCommand(); }); - $(LiveDevelopment).on("statusChange", function statusChange(event, status, reason) { + $(LiveDevImpl).on("statusChange", function statusChange(event, status, reason) { // status starts at -1 (error), so add one when looking up name and style // See the comments at the top of LiveDevelopment.js for details on the // various status codes. @@ -198,11 +200,11 @@ define(function main(require, exports, module) { /** Maintains state of the Live Preview menu item */ function _setupGoLiveMenu() { - $(LiveDevelopment).on("statusChange", function statusChange(event, status) { + $(LiveDevImpl).on("statusChange", function statusChange(event, status) { // Update the checkmark next to 'Live Preview' menu item // Add checkmark when status is STATUS_ACTIVE; otherwise remove it - CommandManager.get(Commands.FILE_LIVE_FILE_PREVIEW).setChecked(status === LiveDevelopment.STATUS_ACTIVE); - CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(status === LiveDevelopment.STATUS_ACTIVE); + CommandManager.get(Commands.FILE_LIVE_FILE_PREVIEW).setChecked(status === LiveDevImpl.STATUS_ACTIVE); + CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(status === LiveDevImpl.STATUS_ACTIVE); }); } @@ -214,13 +216,26 @@ define(function main(require, exports, module) { config.highlight = !config.highlight; _updateHighlightCheckmark(); if (config.highlight) { - LiveDevelopment.showHighlight(); + LiveDevImpl.showHighlight(); } else { - LiveDevelopment.hideHighlight(); + LiveDevImpl.hideHighlight(); } PreferencesManager.setViewState("livedev.highlight", config.highlight); } + + // returns MultiBrowserLiveDev module if livedev.multibrowser pref is set to true or + // LiveDevelopment module (default implementation) in other case + function _getImplementation() { + var impl; + if (PreferencesManager.get('livedev.multibrowser')) { + impl = MultiBrowserLiveDev; + } else { + impl = LiveDevelopment; + } + return impl; + } + /** Setup window references to useful LiveDevelopment modules */ function _setupDebugHelpers() { window.ld = LiveDevelopment; @@ -239,16 +254,19 @@ define(function main(require, exports, module) { AppInit.appReady(function () { params.parse(); + + // init LiveDevelopment + Inspector.init(config); + LiveDevelopment.init(config); - if (!config.multiBrowser) { - // init LiveDevelopment - Inspector.init(config); - LiveDevelopment.init(config); - } else { - // init experimental multi-browser implementation - LiveDevelopment = MultiBrowserLiveDev; - LiveDevelopment.init(config); - } + // init experimental multi-browser implementation + // it can be enable by setting 'livedev.multibrowser' preference to true. + // It has to be initiated at this point in case of dynamically switching + // by changing the preference value. + MultiBrowserLiveDev.init(config); + + // set current active implementation based on pref + LiveDevImpl = _getImplementation(); _loadStyles(); _setupGoLiveButton(); @@ -288,6 +306,15 @@ define(function main(require, exports, module) { "afterFirstLaunch": "user livedev.afterFirstLaunch" }, true); + PreferencesManager.definePreference("livedev.multibrowser", "boolean", false) + .on("change", function () { + // get implementation based on the new value + LiveDevImpl = _getImplementation(); + // restart: it will close the current session and open a + // a new session based on the new selected implementation + _handleGoLiveCommand(); + }); + config.highlight = PreferencesManager.getViewState("livedev.highlight"); // init commands From 05d2601132acca673e54fda283ac75894b079efe Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Wed, 19 Nov 2014 17:48:25 -0300 Subject: [PATCH 04/17] use NativeApp to launch browser Remove logic for launching browser from transport. It now relies on NativeApp.openLiveBrowser --- .../MultiBrowserImpl/protocol/LiveDevProtocol.js | 11 +++++++---- .../transports/NodeSocketTransport.js | 2 +- .../transports/node/NodeSocketTransportDomain.js | 15 +++++---------- .../MultiBrowserImpl/transports/node/package.json | 1 - 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index b3491ea041e..db97317ef3c 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -37,13 +37,15 @@ * * It keeps active connections which are updated when receiving "connect" and "close" from the * underlying transport. Events "Connection.connect"/"Connection.close" are triggered as - * propagation of transport's "connect"/"close". Also proxies "launch" command to transport. + * propagation of transport's "connect"/"close". * */ define(function (require, exports, module) { "use strict"; + var NativeApp = require('utils/NativeApp'); + // Text of the script we'll inject into the browser that handles protocol requests. var LiveDevProtocolRemote = require("text!LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js"), DocumentObserver = require("text!LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js"), @@ -60,7 +62,7 @@ define(function (require, exports, module) { /** * @private * The low-level transport we're communicating over, set by `setTransport()`. - * @type {{launch: function(string), send: function(number|Array., string), close: function(number), getRemoteScript: function(): ?string}} + * @type {{start: function(), send: function(number|Array., string), close: function(number), getRemoteScript: function(): ?string}} */ var _transport = null; @@ -191,6 +193,7 @@ define(function (require, exports, module) { .on("close.livedev", function (event, clientId) { _close(clientId); }); + _transport.start(); } @@ -225,11 +228,11 @@ define(function (require, exports, module) { } /** - * Launches the given URL in the browser. Proxies to the transport. + * Launches the given URL in the browser. * @param {string} url */ function launch(url) { - _transport.launch(url); + NativeApp.openLiveBrowser(url, false); } /** diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js b/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js index c4d6e39ca96..2c72154a289 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport.js @@ -77,7 +77,7 @@ define(function (require, exports, module) { // Proxy the node domain methods directly through, since they have exactly the same // signatures as the ones we're supposed to provide. - ["launch", "send", "close"].forEach(function (method) { + ["start", "send", "close"].forEach(function (method) { exports[method] = function () { var args = Array.prototype.slice.call(arguments); args.unshift(method); diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js b/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js index 33c22c78cba..e98094661d3 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js @@ -27,7 +27,6 @@ "use strict"; var WebSocketServer = require("ws").Server, - open = require("open"), _ = require("lodash"); /** @@ -137,12 +136,11 @@ } /** - * Initializes the socket server, then launches the given URL in the system default browser. + * Initializes the socket server. * @param {string} url */ - function _cmdLaunch(url) { + function _cmdStart(url) { _createServer(); - open(url); } /** @@ -187,13 +185,10 @@ } domainManager.registerCommand( "nodeSocketTransport", // domain name - "launch", // command name - _cmdLaunch, // command handler function + "start", // command name + _cmdStart, // command handler function false, // this command is synchronous in Node - "Launches a given HTML file in the browser for live development", - [{name: "url", // parameters - type: "string", - description: "file:// url to the HTML file"}], + "Creates the WS server", [] ); domainManager.registerCommand( diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json b/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json index 17e04631800..28aff2810e0 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/node/package.json @@ -2,7 +2,6 @@ "name": "brackets-livedev2-server", "dependencies": { "ws": "~0.4.31", - "open": "0.0.4", "lodash": "~2.4.1" } } From 3685a214f04cfd41488410bb4ec415fa7ed20164 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Wed, 19 Nov 2014 18:07:49 -0300 Subject: [PATCH 05/17] fix RemoteFunctions to make it work on IE node.remove() is not supported on IE, using alternative node.parentNode.removeChild(node) in that case --- src/LiveDevelopment/Agents/RemoteFunctions.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/Agents/RemoteFunctions.js b/src/LiveDevelopment/Agents/RemoteFunctions.js index d09d7a0a736..e89d4591feb 100644 --- a/src/LiveDevelopment/Agents/RemoteFunctions.js +++ b/src/LiveDevelopment/Agents/RemoteFunctions.js @@ -660,7 +660,11 @@ function RemoteFunctions(experimental) { } else { lastRemovedWasText = isText; - current.remove(); + if (current.remove) { + current.remove(); + } else if (current.parentNode && current.parentNode.removeChild) { + current.parentNode.removeChild(current); + } current = next; } } @@ -698,7 +702,11 @@ function RemoteFunctions(experimental) { edit.tagIDs.forEach(function (tagID) { var node = self._queryBracketsID(tagID); self.rememberedNodes[tagID] = node; - node.remove(); + if (node.remove) { + node.remove(); + } else if (node.parentNode && node.parentNode.removeChild) { + node.parentNode.removeChild(node); + } }); return; } @@ -720,7 +728,11 @@ function RemoteFunctions(experimental) { targetElement.removeAttribute(edit.attribute); break; case "elementDelete": - targetElement.remove(); + if (targetElement.remove) { + targetElement.remove(); + } else if (targetElement.parentNode && targetElement.parentNode.removeChild) { + targetElement.parentNode.removeChild(targetElement); + } break; case "elementInsert": childElement = null; From d0c9981b377155e006385cb406f54aa2a0184ad6 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Thu, 20 Nov 2014 17:25:29 -0300 Subject: [PATCH 06/17] add some tests under livepreview category Add integration tests for initial connection and related documents. --- src/LiveDevelopment/LiveDevMultiBrowser.js | 6 +- src/brackets.js | 1 + test/UnitTestSuite.js | 1 + .../import1.css | 3 + .../simple1.css | 6 + .../simple1.html | 15 ++ .../simple1.js | 0 .../simpleShared.css | 4 + test/spec/LiveDevelopmentMultiBrowser-test.js | 227 ++++++++++++++++++ 9 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 test/spec/LiveDevelopment-MultiBrowser-test-files/import1.css create mode 100644 test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.css create mode 100644 test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.html create mode 100644 test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.js create mode 100644 test/spec/LiveDevelopment-MultiBrowser-test-files/simpleShared.css create mode 100644 test/spec/LiveDevelopmentMultiBrowser-test.js diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 8988a339df8..98e077682cb 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -828,9 +828,13 @@ define(function (require, exports, module) { return _server && _server.getBaseUrl(); } + // for unit testing only + function _getCurrentLiveDoc() { + return _liveDocument; + } // For unit testing exports._server = _server; - exports._liveDocument = _liveDocument; + exports._getCurrentLiveDoc = _getCurrentLiveDoc; exports._getInitialDocFromCurrent = _getInitialDocFromCurrent; // Export public functions diff --git a/src/brackets.js b/src/brackets.js index e0d5e728334..1a78a1f5502 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -191,6 +191,7 @@ define(function (require, exports, module) { KeyBindingManager : KeyBindingManager, LanguageManager : LanguageManager, LiveDevelopment : require("LiveDevelopment/LiveDevelopment"), + LiveDevMultiBrowser : require("LiveDevelopment/LiveDevMultiBrowser"), LiveDevServerManager : require("LiveDevelopment/LiveDevServerManager"), MainViewManager : MainViewManager, MainViewFactory : require("view/MainViewFactory"), diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index 7d710ef966d..7a867b3dc5e 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -62,6 +62,7 @@ define(function (require, exports, module) { require("spec/KeyBindingManager-test"); require("spec/LanguageManager-test"); require("spec/LiveDevelopment-test"); + require("spec/LiveDevelopmentMultiBrowser-test"); require("spec/LowLevelFileIO-test"); require("spec/MainViewFactory-test"); require("spec/MainViewManager-test"); diff --git a/test/spec/LiveDevelopment-MultiBrowser-test-files/import1.css b/test/spec/LiveDevelopment-MultiBrowser-test-files/import1.css new file mode 100644 index 00000000000..9b001e53e6e --- /dev/null +++ b/test/spec/LiveDevelopment-MultiBrowser-test-files/import1.css @@ -0,0 +1,3 @@ +.testClass2 { + color: #000; +} \ No newline at end of file diff --git a/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.css b/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.css new file mode 100644 index 00000000000..03171114c35 --- /dev/null +++ b/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.css @@ -0,0 +1,6 @@ +@import url('import1.css'); + +.testClass { + color: #000; +} + diff --git a/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.html b/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.html new file mode 100644 index 00000000000..ac9451e2225 --- /dev/null +++ b/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.html @@ -0,0 +1,15 @@ + + + + +Simple Test + + + + + + +

Brackets is awesome!

+

Red is bad. Green is good.

+ + diff --git a/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.js b/test/spec/LiveDevelopment-MultiBrowser-test-files/simple1.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/spec/LiveDevelopment-MultiBrowser-test-files/simpleShared.css b/test/spec/LiveDevelopment-MultiBrowser-test-files/simpleShared.css new file mode 100644 index 00000000000..12f7b219774 --- /dev/null +++ b/test/spec/LiveDevelopment-MultiBrowser-test-files/simpleShared.css @@ -0,0 +1,4 @@ +body { + background-color: red; +} + diff --git a/test/spec/LiveDevelopmentMultiBrowser-test.js b/test/spec/LiveDevelopmentMultiBrowser-test.js new file mode 100644 index 00000000000..d6917378f05 --- /dev/null +++ b/test/spec/LiveDevelopmentMultiBrowser-test.js @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint evil: true */ + +/*global $, brackets, define, describe, xdescribe, it, xit, expect, beforeEach, afterEach, beforeFirst, afterLast, waitsFor, waitsForDone, runs, window, spyOn, jasmine */ + +define(function (require, exports, module) { + "use strict"; + + var SpecRunnerUtils = require("spec/SpecRunnerUtils"); + + describe("MultiBrowser (experimental) - LiveDevelopment", function () { + + this.category = "livepreview"; + + var testWindow, + brackets, + CommandManager, + Commands, + EditorManager, + DocumentManager, + LiveDevelopment, + LiveDevProtocol, + LiveHTMLDocument, + PreferencesManager, + editor; + + var testFolder = SpecRunnerUtils.getTestPath("/spec/LiveDevelopment-MultiBrowser-test-files"), + allSpacesRE = /\s+/gi; + + beforeEach(function () { + // Create a new window that will be shared by ALL tests in this spec. + if (!testWindow) { + runs(function () { + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + testWindow = w; + // Load module instances from brackets.test + brackets = testWindow.brackets; + CommandManager = brackets.test.CommandManager; + Commands = brackets.test.Commands; + EditorManager = brackets.test.EditorManager; + DocumentManager = brackets.test.DocumentManager; + LiveDevelopment = brackets.test.LiveDevMultiBrowser; + LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"); + PreferencesManager = brackets.test.PreferencesManager; + }); + }); + + runs(function () { + SpecRunnerUtils.loadProjectInTestWindow(testFolder); + }); + } + }); + + afterEach(function () { + SpecRunnerUtils.closeTestWindow(); + testWindow = null; + brackets = null; + LiveDevelopment = null; + LiveDevProtocol = null; + }); + + function waitsForLiveDevelopmentToOpen() { + runs(function () { + LiveDevelopment.open(); + }); + waitsFor( + function isLiveDevelopmentActive() { + return LiveDevelopment.status === LiveDevelopment.STATUS_ACTIVE; + }, + "livedevelopment.done.opened", + 5000 + ); + } + + describe("Init Session", function () { + + + it("should establish a browser connection for an opened html file", function () { + //open a file + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + + waitsForLiveDevelopmentToOpen(); + + runs(function () { + expect(LiveDevelopment.status).toBe(LiveDevelopment.STATUS_ACTIVE); + }); + }); + + it("should send all external stylesheets as related docs on start-up", function () { + var liveDoc; + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + waitsForLiveDevelopmentToOpen(); + runs(function () { + liveDoc = LiveDevelopment._getCurrentLiveDoc(); + }); + waitsFor( + function relatedDocsReceived() { + return (Object.getOwnPropertyNames(liveDoc.getRelated().stylesheets).length > 0); + }, + "relatedDocuments.done.received", + 10000 + ); + runs(function () { + expect(liveDoc.isRelated(testFolder + "/simple1.css")).toBeTruthy(); + }); + runs(function () { + expect(liveDoc.isRelated(testFolder + "/simpleShared.css")).toBeTruthy(); + }); + }); + + it("should send all import-ed stylesheets as related docs on start-up", function () { + var liveDoc; + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + waitsForLiveDevelopmentToOpen(); + runs(function () { + liveDoc = LiveDevelopment._getCurrentLiveDoc(); + }); + waitsFor( + function relatedDocsReceived() { + return (Object.getOwnPropertyNames(liveDoc.getRelated().scripts).length > 0); + }, + "relatedDocuments.done.received", + 10000 + ); + runs(function () { + expect(liveDoc.isRelated(testFolder + "/import1.css")).toBeTruthy(); + }); + }); + + it("should send all external javascript files as related docs on start-up", function () { + var liveDoc; + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + waitsForLiveDevelopmentToOpen(); + + runs(function () { + liveDoc = LiveDevelopment._getCurrentLiveDoc(); + }); + waitsFor( + function relatedDocsReceived() { + return (Object.getOwnPropertyNames(liveDoc.getRelated().scripts).length > 0); + }, + "relatedDocuments.done.received", + 10000 + ); + runs(function () { + expect(liveDoc.isRelated(testFolder + "/simple1.js")).toBeTruthy(); + }); + }); + + it("should send notifications for added/removed stylesheets through link nodes", function () { + var liveDoc; + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + waitsForLiveDevelopmentToOpen(); + + runs(function () { + liveDoc = LiveDevelopment._getCurrentLiveDoc(); + }); + + runs(function () { + var curDoc = DocumentManager.getCurrentDocument(); + curDoc.replaceRange('\n', {line: 8, ch: 0}); + }); + + waitsFor( + function relatedDocsReceived() { + return (Object.getOwnPropertyNames(liveDoc.getRelated().stylesheets).length === 4); + }, + "relatedDocuments.done.received", + 10000 + ); + + runs(function () { + expect(liveDoc.isRelated(testFolder + "/simple2.css")).toBeTruthy(); + }); + + runs(function () { + var curDoc = DocumentManager.getCurrentDocument(); + curDoc.replaceRange('', {line: 8, ch: 0}, {line: 8, ch: 50}); + }); + + waitsFor( + function relatedDocsReceived() { + return (Object.getOwnPropertyNames(liveDoc.getRelated().stylesheets).length === 3); + }, + "relatedDocuments.done.received", + 10000 + ); + + runs(function () { + expect(liveDoc.isRelated(testFolder + "/simple2.css")).toBeFalsy(); + }); + }); + }); + }); +}); From dd6ca829b89f7138d196b69fc8339941cbfae571 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Thu, 20 Nov 2014 17:52:27 -0300 Subject: [PATCH 07/17] Add launch/close browser to LiveDevelopment - Add close window based on NativeApp.closeLiveBrowser for mac - Move launch from protocol to LiveDevelopment based since already relies on NativeApp.openLiveBrowser --- src/LiveDevelopment/LiveDevMultiBrowser.js | 28 +++++++++++++++---- .../protocol/LiveDevProtocol.js | 12 +------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 98e077682cb..a9308f79329 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -81,9 +81,11 @@ define(function (require, exports, module) { ExtensionUtils = require("utils/ExtensionUtils"), FileSystemError = require("filesystem/FileSystemError"), FileUtils = require("file/FileUtils"), + NativeApp = require("utils/NativeApp"), PreferencesDialogs = require("preferences/PreferencesDialogs"), ProjectManager = require("project/ProjectManager"), Strings = require("strings"), + StringUtils = require("utils/StringUtils"), _ = require("thirdparty/lodash"), LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"), NodeSocketTransport = require("LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport"), @@ -480,11 +482,11 @@ define(function (require, exports, module) { _server = null; } } - - // TODO: don't have a way to close windows in the new architecture -// if (doCloseWindow) { -// } - + if (doCloseWindow) { + if (brackets.platform === "mac") { + NativeApp.closeLiveBrowser(); + } + } _setStatus(STATUS_INACTIVE, reason || "explicit_close"); } @@ -532,6 +534,20 @@ define(function (require, exports, module) { _server.add(_liveDocument); } + + /** + * Launches the given URL in the default browser. + * @param {string} url + * TODO: launchers for multiple browsers + */ + function _launch(url) { + // open default browser + // TODO: fail? + // + NativeApp.openLiveBrowser(url, false); + + } + /** * @private * Launches the given document in the browser, given that a live document has already @@ -544,7 +560,7 @@ define(function (require, exports, module) { // 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) { - _protocol.launch(_server.pathToUrl(doc.file.fullPath)); + _launch(_server.pathToUrl(doc.file.fullPath)); } if (exports.status === STATUS_RESTARTING) { // change page in browser diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index db97317ef3c..54ec1309a43 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -176,7 +176,7 @@ define(function (require, exports, module) { /** * Sets the transport that should be used by the protocol. See `LiveDevelopment.setTransport()` * for more detail on the transport. - * @param {{launch: function(string), send: function(number|Array., string), close: function(number), getRemoteScript: function(): ?string}} transport + * @param {{start: function(string), send: function(number|Array., string), close: function(number), getRemoteScript: function(): ?string}} transport */ function setTransport(transport) { if (_transport) { @@ -224,15 +224,6 @@ define(function (require, exports, module) { return transportScript + "\n" + remoteFunctionsScript; - - } - - /** - * Launches the given URL in the browser. - * @param {string} url - */ - function launch(url) { - NativeApp.openLiveBrowser(url, false); } /** @@ -312,7 +303,6 @@ define(function (require, exports, module) { // public API exports.setTransport = setTransport; exports.getRemoteScript = getRemoteScript; - exports.launch = launch; exports.evaluate = evaluate; exports.reload = reload; exports.navigate = navigate; From 56db21eb5f5f77f62200d4dd5fbf06ac7c8088f1 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Thu, 20 Nov 2014 20:35:07 -0300 Subject: [PATCH 08/17] unify reloadCSS with remote protocol commands - move reloadCSS implementation from ExtendedRemoteFunctions to LiveDevProtocolremote with the rest of the remote commands - add method setStylesheetText to protocol to make it more representative of the implementation and also align it with CDT name - align LiveCSSDocument to the new protocol method --- .../documents/LiveCSSDocument.js | 4 +- .../protocol/LiveDevProtocol.js | 16 ++++- .../remote/ExtendedRemoteFunctions.js | 63 ------------------- .../protocol/remote/LiveDevProtocolRemote.js | 39 ++++++++++++ 4 files changed, 54 insertions(+), 68 deletions(-) delete mode 100644 src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js index d7b914b137c..7e2a166a420 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js @@ -99,9 +99,7 @@ define(function LiveCSSDocumentModule(require, exports, module) { // reload the original doc $(this).triggerHandler("updateDoc", this.roots[i]); } else { - this.protocol.evaluate("_LD.reloadCSS(" + - JSON.stringify(this.doc.url) + ", " + - JSON.stringify(this.doc.getText()) + ")"); + this.protocol.setStylesheetText(this.doc.url, this.doc.getText()); } } this.redrawHighlights(); diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 54ec1309a43..2d1135551f4 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -49,7 +49,6 @@ define(function (require, exports, module) { // Text of the script we'll inject into the browser that handles protocol requests. var LiveDevProtocolRemote = require("text!LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js"), DocumentObserver = require("text!LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js"), - AddedRemoteFunctions = require("text!LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js"), RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"); /** @@ -208,7 +207,7 @@ define(function (require, exports, module) { // Inject DocumentObserver into the browser (tracks related documents) script += DocumentObserver; // Inject remote functions into the browser. - script += "window._LD=" + AddedRemoteFunctions + "(" + RemoteFunctions + "())"; + script += "window._LD=(" + RemoteFunctions + "())"; return "\n"; } @@ -247,6 +246,18 @@ define(function (require, exports, module) { ); } + function setStylesheetText(url, text, clients) { + return _send( + { + method: "CSS.setStylesheetText", + params: { + url: url, + text: text + } + } + ); + } + /** * Protocol method. Reloads the page that is currently loaded into the browser, optionally ignoring cache. * @param {number|Array.} clients A client ID or array of client IDs that should reload the page. @@ -304,6 +315,7 @@ define(function (require, exports, module) { exports.setTransport = setTransport; exports.getRemoteScript = getRemoteScript; exports.evaluate = evaluate; + exports.setStylesheetText = setStylesheetText; exports.reload = reload; exports.navigate = navigate; exports.close = close; diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js deleted file mode 100644 index 8ca1671595b..00000000000 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/ExtendedRemoteFunctions.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2014 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, browser: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ -/*global define, $, window, navigator, Node, console */ - -/** - * ExtendRemoteFunctions defines the addtional functions to be executed in the browser. - */ -function ExtendRemoteFunctions(obj) { - "use strict"; - - var ExtendedObj = function () {}; - ExtendedObj.prototype = obj; - - ExtendedObj.prototype.reloadCSS = function reloadCSS(url, text) { - var i, - node; - - var head = document.getElementsByTagName('head')[0]; - // create an style element to replace the one loaded with - var s = document.createElement('style'); - s.type = 'text/css'; - s.appendChild(document.createTextNode(text)); - - for (i = 0; i < document.styleSheets.length; i++) { - node = document.styleSheets[i]; - if (node.ownerNode.id === url) { - head.insertBefore(s, node.ownerNode); // insert the style element here - // now can remove the style element previously created (if any) - node.ownerNode.parentNode.removeChild(node.ownerNode); - } else if (node.href === url && !node.disabled) { - // if the link element to change - head.insertBefore(s, node.ownerNode); // insert the style element here - node.disabled = true; - i++; // since we have just inserted a stylesheet - } - } - s.id = url; - }; - return new ExtendedObj(); -} \ No newline at end of file diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js index b7a5e08fae1..cad408d16d4 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js @@ -151,6 +151,45 @@ // subscribe handler to method Runtime.evaluate MessageBroker.on("Runtime.evaluate", Runtime.evaluate); + /** + * CSS Domain. + */ + var CSS = { + + setStylesheetText : function (msg) { + + if (!msg || !msg.params || !msg.params.text || !msg.params.url) { + return; + } + + var i, + node; + + var head = document.getElementsByTagName('head')[0]; + // create an style element to replace the one loaded with + var s = document.createElement('style'); + s.type = 'text/css'; + s.appendChild(document.createTextNode(msg.params.text)); + + for (i = 0; i < document.styleSheets.length; i++) { + node = document.styleSheets[i]; + if (node.ownerNode.id === msg.params.url) { + head.insertBefore(s, node.ownerNode); // insert the style element here + // now can remove the style element previously created (if any) + node.ownerNode.parentNode.removeChild(node.ownerNode); + } else if (node.href === msg.params.url && !node.disabled) { + // if the link element to change + head.insertBefore(s, node.ownerNode); // insert the style element here + node.disabled = true; + i++; // since we have just inserted a stylesheet + } + } + s.id = msg.params.url; + } + }; + + MessageBroker.on("CSS.setStylesheetText", CSS.setStylesheetText); + /** * Page Domain. */ From 19419aca7865761f0b8b4df9b5b2b146a63c256c Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 07:20:03 -0300 Subject: [PATCH 09/17] Add missing documentation for setStylesheetText --- .../MultiBrowserImpl/protocol/LiveDevProtocol.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 2d1135551f4..3f0c9e40602 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -246,6 +246,15 @@ define(function (require, exports, module) { ); } + /** + * Protocol method. Reloads a CSS styleseet in the browser (by replacing its text) given its url. + * @param {string} url Absolute URL of the stylesheet + * @param {string} text The new text of the stylesheet + * @param {number|Array.} clients A client ID or array of client IDs that should evaluate + * the script. + * @return {$.Promise} A promise that's resolved with the return value from the first client that responds + * to the evaluation. + */ function setStylesheetText(url, text, clients) { return _send( { From 578805b4d1925883c61f86f0f0f62813ecbeb7fe Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 09:09:07 -0300 Subject: [PATCH 10/17] fix some issues on CSS support - nodes created with new stylesheet text are now removed when the stylesheet is removed - make updates on import-ed CSS work in Firefox --- .../documents/LiveCSSDocument.js | 2 +- .../protocol/remote/DocumentObserver.js | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js index 7e2a166a420..9bc7f5299ee 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js @@ -94,7 +94,7 @@ define(function LiveCSSDocumentModule(require, exports, module) { LiveCSSDocument.prototype._updateBrowser = function () { var i; for (i = 0; i < this.roots.length; i++) { - if (this.doc.url !== this.roots[i]) { + if (this.doc.url !== this.roots[i].toString()) { // if it's not directly included through , // reload the original doc $(this).triggerHandler("updateDoc", this.roots[i]); diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js index 254c6e3d7e6..5e60f5a100c 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/DocumentObserver.js @@ -91,6 +91,16 @@ } }, 50); }, + + onStylesheetRemoved : function (url) { + // get style node created when setting new text for stylesheet. + var s = document.getElementById(url); + // remove + if (s && s.parentNode && s.parentNode.removeChild) { + s.parentNode.removeChild(s); + } + }, + /** * Send a notification for the stylesheet added and * its import-ed styleshets based on document.stylesheets diff @@ -115,7 +125,7 @@ _transport.send(JSON.stringify({ method: "Stylesheet.Added", href: v, - roots: added[v] + roots: [added[v]] })); }); @@ -128,17 +138,21 @@ * from previous status. It also updates stylesheets status. */ notifyStylesheetRemoved : function () { + + var self = this; var i, removed = {}, newStatus, current; - current = this.stylesheets; + current = self.stylesheets; newStatus = related().stylesheets; Object.keys(current).forEach(function (v, i) { if (!newStatus[v]) { removed[v] = current[v]; + // remove node created by setStylesheetText if any + self.onStylesheetRemoved(current[v]); } }); @@ -146,11 +160,11 @@ _transport.send(JSON.stringify({ method: "Stylesheet.Removed", href: v, - roots: removed[v] + roots: [removed[v]] })); }); - this.stylesheets = newStatus; + self.stylesheets = newStatus; } }; From e3a6c40a3375daa985176169ea6946145fa824ae Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 12:14:30 -0300 Subject: [PATCH 11/17] add new test for CSS editing It also implements getStylesheetText on remote protocol and fixes respond (there was no previous usage of it). --- .../documents/LiveCSSDocument.js | 12 +++++ .../protocol/LiveDevProtocol.js | 20 +++++++++ .../protocol/remote/LiveDevProtocolRemote.js | 30 ++++++++++++- test/spec/LiveDevelopmentMultiBrowser-test.js | 44 +++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js index 9bc7f5299ee..201ddc59039 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument.js @@ -165,6 +165,18 @@ define(function LiveCSSDocumentModule(require, exports, module) { $(this).triggerHandler("deleted", [this]); }; + // Only used for unit testing. + LiveCSSDocument.prototype.getSourceFromBrowser = function () { + var deferred = new $.Deferred(); + + this.protocol.getStylesheetText(this.doc.url) + .then(function (res) { + deferred.resolve(res.text); + }, deferred.reject); + + return deferred.promise(); + }; + // Export the class module.exports = LiveCSSDocument; }); \ No newline at end of file diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 3f0c9e40602..bedb055b77f 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -267,6 +267,25 @@ define(function (require, exports, module) { ); } + /** + * Protocol method. Rretrieves the content of a given stylesheet (for unit testing) + * @param {number|Array.} clients A client ID or array of client IDs that should navigate to the given URL. + * @param {string} url Absolute URL that identifies the stylesheet. + * @return {$.Promise} A promise that's resolved with the return value from the first client that responds + * to the method. + */ + function getStylesheetText(url, clients) { + return _send( + { + method: "CSS.getStylesheetText", + params: { + url: url + } + }, + clients + ); + } + /** * Protocol method. Reloads the page that is currently loaded into the browser, optionally ignoring cache. * @param {number|Array.} clients A client ID or array of client IDs that should reload the page. @@ -325,6 +344,7 @@ define(function (require, exports, module) { exports.getRemoteScript = getRemoteScript; exports.evaluate = evaluate; exports.setStylesheetText = setStylesheetText; + exports.getStylesheetText = getStylesheetText; exports.reload = reload; exports.navigate = navigate; exports.close = close; diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js index cad408d16d4..390b9b6cb57 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/remote/LiveDevProtocolRemote.js @@ -102,7 +102,7 @@ return; } response.id = orig.id; - this.send(JSON.stringify(response)); + this.send(response); }, /** @@ -185,10 +185,38 @@ } } s.id = msg.params.url; + }, + + /** + * retrieves the content of the stylesheet + * TODO: it now depends on reloadCSS implementation + */ + getStylesheetText: function (msg) { + var i, + sheet, + text; + for (i = 0; i < document.styleSheets.length; i++) { + sheet = document.styleSheets[i]; + // if it was already 'reloaded' + if (sheet.ownerNode.id === msg.params.url) { + text = sheet.ownerNode.innerText; + } else if (sheet.href === msg.params.url && !sheet.disabled) { + var j, + rules = document.styleSheets[i].cssRules; + text = ""; + for (j = 0; j < rules.length; j++) { + text += rules[j].cssText + '\n'; + } + } + } + MessageBroker.respond(msg, { + text: text + }); } }; MessageBroker.on("CSS.setStylesheetText", CSS.setStylesheetText); + MessageBroker.on("CSS.getStylesheetText", CSS.getStylesheetText); /** * Page Domain. diff --git a/test/spec/LiveDevelopmentMultiBrowser-test.js b/test/spec/LiveDevelopmentMultiBrowser-test.js index d6917378f05..02afe32f1be 100644 --- a/test/spec/LiveDevelopmentMultiBrowser-test.js +++ b/test/spec/LiveDevelopmentMultiBrowser-test.js @@ -49,6 +49,10 @@ define(function (require, exports, module) { var testFolder = SpecRunnerUtils.getTestPath("/spec/LiveDevelopment-MultiBrowser-test-files"), allSpacesRE = /\s+/gi; + function fixSpaces(str) { + return str.replace(allSpacesRE, " "); + } + beforeEach(function () { // Create a new window that will be shared by ALL tests in this spec. if (!testWindow) { @@ -222,6 +226,46 @@ define(function (require, exports, module) { expect(liveDoc.isRelated(testFolder + "/simple2.css")).toBeFalsy(); }); }); + + + it("should push changes through browser connection when editing a related CSS", function () { + var localText, + browserText, + liveDoc, + curDoc; + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + + waitsForLiveDevelopmentToOpen(); + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); + }); + runs(function () { + curDoc = DocumentManager.getCurrentDocument(); + localText = curDoc.getText(); + localText += "\n .testClass { background-color:#090; }\n"; + curDoc.setText(localText); + }); + runs(function () { + liveDoc = LiveDevelopment.getLiveDocForPath(testFolder + "/simple1.css"); + }); + var doneSyncing = false; + runs(function () { + liveDoc.getSourceFromBrowser().done(function (text) { + browserText = text; + }).always(function () { + doneSyncing = true; + }); + }); + waitsFor(function () { return doneSyncing; }, "Browser to sync changes", 5000); + + runs(function () { + expect(fixSpaces(browserText)).toBe(fixSpaces(localText)); + }); + }); }); }); }); From e8ee4d8d5f6101af2ccb631cf9fa74be98156fbb Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 15:59:06 -0300 Subject: [PATCH 12/17] add Launcher now just for default browser --- src/LiveDevelopment/LiveDevMultiBrowser.js | 8 +- .../MultiBrowserImpl/launchers/Launcher.js | 55 +++++++++++++ .../launchers/node/LauncherDomain.js | 80 +++++++++++++++++++ .../launchers/node/package.json | 6 ++ 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js create mode 100644 src/LiveDevelopment/MultiBrowserImpl/launchers/node/package.json diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index a9308f79329..a7de3b59907 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -89,7 +89,8 @@ define(function (require, exports, module) { _ = require("thirdparty/lodash"), LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"), NodeSocketTransport = require("LiveDevelopment/MultiBrowserImpl/transports/NodeSocketTransport"), - LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"); + LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"), + Launcher = require("LiveDevelopment/MultiBrowserImpl/launchers/Launcher"); // Documents var LiveCSSDocument = require("LiveDevelopment/MultiBrowserImpl/documents/LiveCSSDocument"), @@ -113,7 +114,7 @@ define(function (require, exports, module) { /** * @private * Current transport for communicating with browser instances. See setTransport(). - * @type {{launch: function(string), send: function(number|Array., string), close: function(number)}} + * @type {{start: function(string), send: function(number|Array., string), close: function(number)}} */ var _transport; @@ -544,8 +545,7 @@ define(function (require, exports, module) { // open default browser // TODO: fail? // - NativeApp.openLiveBrowser(url, false); - + Launcher.launch(url); } /** diff --git a/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js b/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js new file mode 100644 index 00000000000..54f1655e9e9 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ +/*global define, $, brackets, window, open */ + + +define(function (require, exports, module) { + "use strict"; + + var DEFAULT_BROWSER = exports.DEFAULT_BROWSER = 'default'; + + var FileUtils = require("file/FileUtils"), + NodeDomain = require("utils/NodeDomain"); + + var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), + _modulePath = FileUtils.getNativeModuleDirectoryPath(module), + _nodePath = "node/LauncherDomain", + _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), + _nodeDomain = new NodeDomain("launcher", _domainPath); + + function launch(url, browser) { + if (!browser) { + browser = DEFAULT_BROWSER; + } + // launch from node domain + _nodeDomain.exec("launch", url, browser); + console.log("launching " + browser); + } + + + // Exports + exports.launch = launch; + +}); \ No newline at end of file diff --git a/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js new file mode 100644 index 00000000000..a72ba472be7 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, node: true */ + +(function () { + "use strict"; + + var open = require("open"); + + /** + * @private + * The Brackets domain manager for registering node extensions. + * @type {?DomainManager} + */ + var _domainManager; + + /** + * @private + * A map of client IDs to the URL and WebSocket for the given ID. + * @type {Object.} + */ + var _browsers = {}; + + /** + * Launches the given URL in the given browser. + * TODO: it now launching just on default system, add launchers for specific browsers. + * @param {string} url + */ + function _cmdLaunch(url, browser) { + open(url); + } + + + /** + * Initializes the domain and registers commands. + * @param {DomainManager} domainManager The DomainManager for the server + */ + function init(domainManager) { + _domainManager = domainManager; + if (!domainManager.hasDomain("launcher")) { + domainManager.registerDomain("launcher", {major: 0, minor: 1}); + } + domainManager.registerCommand( + "launcher", // domain name + "launch", // command name + _cmdLaunch, // command handler function + false, // this command is synchronous in Node + "Launches a given HTML file in the browser for live development", + [ + { name: "url", type: "string", description: "file:// url to the HTML file" }, + { name: "browser", type: "string", description: "browser name"} + ], + [] + ); + } + + exports.init = init; + +}()); diff --git a/src/LiveDevelopment/MultiBrowserImpl/launchers/node/package.json b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/package.json new file mode 100644 index 00000000000..83cdf737498 --- /dev/null +++ b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/package.json @@ -0,0 +1,6 @@ +{ + "name": "livedev-multibrowser-launchers", + "dependencies": { + "open": "0.0.4" + } +} From bb0ab91da3d91df9950732ca28b772a3935e3390 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 15:59:40 -0300 Subject: [PATCH 13/17] add new test test for pushing in-memory changes --- test/spec/LiveDevelopmentMultiBrowser-test.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/spec/LiveDevelopmentMultiBrowser-test.js b/test/spec/LiveDevelopmentMultiBrowser-test.js index 02afe32f1be..b597da85780 100644 --- a/test/spec/LiveDevelopmentMultiBrowser-test.js +++ b/test/spec/LiveDevelopmentMultiBrowser-test.js @@ -266,6 +266,50 @@ define(function (require, exports, module) { expect(fixSpaces(browserText)).toBe(fixSpaces(localText)); }); }); + + it("should push in memory css changes made before the session starts", function () { + var localText, + browserText; + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); + }); + + runs(function () { + var curDoc = DocumentManager.getCurrentDocument(); + localText = curDoc.getText(); + localText += "\n .testClass { background-color:#090; }\n"; + curDoc.setText(localText); + + // Document should not be marked dirty + expect(LiveDevelopment.status).not.toBe(LiveDevelopment.STATUS_OUT_OF_SYNC); + }); + + runs(function () { + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + }); + + waitsForLiveDevelopmentToOpen(); + + + var liveDoc, doneSyncing = false; + runs(function () { + liveDoc = LiveDevelopment.getLiveDocForPath(testFolder + "/simple1.css"); + }); + + runs(function () { + liveDoc.getSourceFromBrowser().done(function (text) { + browserText = text; + }).always(function () { + doneSyncing = true; + }); + }); + waitsFor(function () { return doneSyncing; }, "Browser to sync changes", 10000); + + runs(function () { + expect(fixSpaces(browserText)).toBe(fixSpaces(localText)); + }); + }); }); }); }); From 8560dcd158b1508a4cb407be29054f5b54d1283a Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Fri, 21 Nov 2014 19:00:01 -0300 Subject: [PATCH 14/17] add UI status for MultiBrowserImpl --- src/LiveDevelopment/main.js | 74 ++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 44bd0a89666..a3aa25a286a 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -68,21 +68,13 @@ define(function main(require, exports, module) { } }; // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. - var _statusTooltip = [ - Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, - Strings.LIVE_DEV_STATUS_TIP_PROGRESS2, - Strings.LIVE_DEV_STATUS_TIP_CONNECTED, - Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, - Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR - ]; - - var _statusStyle = ["warning", "", "info", "info", "success", "out-of-sync", "sync-error"]; // Status indicator's CSS class - var _allStatusStyles = _statusStyle.join(" "); + var _statusTooltip, + _statusStyle, + _allStatusStyles; var _$btnGoLive; // reference to the GoLive button + // current selected implementation (LiveDevelopment | LiveDevMultiBrowser) var LiveDevImpl; /** Load Live Development LESS Style */ @@ -128,7 +120,6 @@ define(function main(require, exports, module) { * Do nothing when in a connecting state (CONNECTING, LOADING_AGENTS). */ function _handleGoLiveCommand() { - if (LiveDevImpl.status >= LiveDevImpl.STATUS_ACTIVE) { LiveDevImpl.close(); } else if (LiveDevImpl.status <= LiveDevImpl.STATUS_INACTIVE) { @@ -223,17 +214,41 @@ define(function main(require, exports, module) { PreferencesManager.setViewState("livedev.highlight", config.highlight); } - - // returns MultiBrowserLiveDev module if livedev.multibrowser pref is set to true or - // LiveDevelopment module (default implementation) in other case - function _getImplementation() { - var impl; - if (PreferencesManager.get('livedev.multibrowser')) { - impl = MultiBrowserLiveDev; + // sets the MultiBrowserLiveDev implementation if multibrowser = true, + // keeps default LiveDevelopment implementation based on CDT in other case. + // since UI status are slightly different btw implementations, it also set + // the corresponding style values. + function _setImplementation(multibrowser) { + console.log('set implementation ' + multibrowser); + if (multibrowser) { + // set implemenation + LiveDevImpl = MultiBrowserLiveDev; + // update styles for UI status + _statusTooltip = [ + Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, + Strings.LIVE_DEV_STATUS_TIP_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, + Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR, + Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, + Strings.LIVE_DEV_STATUS_TIP_PROGRESS1 + ]; + _statusStyle = ["warning", "", "info", "success", "out-of-sync", "sync-error", "info", "info"]; // Status indicator's CSS class } else { - impl = LiveDevelopment; + LiveDevImpl = LiveDevelopment; + _statusTooltip = [ + Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, + Strings.LIVE_DEV_STATUS_TIP_PROGRESS2, + Strings.LIVE_DEV_STATUS_TIP_CONNECTED, + Strings.LIVE_DEV_STATUS_TIP_OUT_OF_SYNC, + Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR + ]; + _statusStyle = ["warning", "", "info", "info", "success", "out-of-sync", "sync-error"]; // Status indicator's CSS class } - return impl; + _allStatusStyles = _statusStyle.join(" "); } /** Setup window references to useful LiveDevelopment modules */ @@ -265,9 +280,6 @@ define(function main(require, exports, module) { // by changing the preference value. MultiBrowserLiveDev.init(config); - // set current active implementation based on pref - LiveDevImpl = _getImplementation(); - _loadStyles(); _setupGoLiveButton(); _setupGoLiveMenu(); @@ -308,11 +320,13 @@ define(function main(require, exports, module) { PreferencesManager.definePreference("livedev.multibrowser", "boolean", false) .on("change", function () { - // get implementation based on the new value - LiveDevImpl = _getImplementation(); - // restart: it will close the current session and open a - // a new session based on the new selected implementation - _handleGoLiveCommand(); + // stop the current session if it is open and set implementation based on + // the pref value. It could be automaticallty restarted but, since the preference file, + // is the document open in the editor, it will no launch a live document. + if (LiveDevImpl && LiveDevImpl.status >= LiveDevImpl.STATUS_ACTIVE) { + LiveDevImpl.close(); + } + _setImplementation(PreferencesManager.get('livedev.multibrowser')); }); config.highlight = PreferencesManager.getViewState("livedev.highlight"); From 6b8aa7b419fda4761b5053430a068239c9b3005f Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 24 Nov 2014 11:14:53 -0300 Subject: [PATCH 15/17] clean-up --- .../MultiBrowserImpl/launchers/Launcher.js | 16 +++++++--------- .../launchers/node/LauncherDomain.js | 15 ++++----------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js b/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js index 54f1655e9e9..60407cc6bdd 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js +++ b/src/LiveDevelopment/MultiBrowserImpl/launchers/Launcher.js @@ -28,8 +28,6 @@ define(function (require, exports, module) { "use strict"; - var DEFAULT_BROWSER = exports.DEFAULT_BROWSER = 'default'; - var FileUtils = require("file/FileUtils"), NodeDomain = require("utils/NodeDomain"); @@ -39,15 +37,15 @@ define(function (require, exports, module) { _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), _nodeDomain = new NodeDomain("launcher", _domainPath); - function launch(url, browser) { - if (!browser) { - browser = DEFAULT_BROWSER; - } + + /** + * Launch the given URL in the system default browser. + * @param {string} url + */ + function launch(url) { // launch from node domain - _nodeDomain.exec("launch", url, browser); - console.log("launching " + browser); + _nodeDomain.exec("launch", url); } - // Exports exports.launch = launch; diff --git a/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js index a72ba472be7..8791a2a79cc 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js +++ b/src/LiveDevelopment/MultiBrowserImpl/launchers/node/LauncherDomain.js @@ -34,20 +34,13 @@ * @type {?DomainManager} */ var _domainManager; - - /** - * @private - * A map of client IDs to the URL and WebSocket for the given ID. - * @type {Object.} - */ - var _browsers = {}; /** - * Launches the given URL in the given browser. - * TODO: it now launching just on default system, add launchers for specific browsers. + * Launch the given URL in the system default browser. + * TODO: it now launching just on default browser, add launchers for specific browsers. * @param {string} url */ - function _cmdLaunch(url, browser) { + function _cmdLaunch(url) { open(url); } @@ -74,7 +67,7 @@ [] ); } - + exports.init = init; }()); From d17c0de5e50502653b3921462b7c12018b05181c Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 24 Nov 2014 11:28:33 -0300 Subject: [PATCH 16/17] update README.md --- .../MultiBrowserImpl/README.md | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/README.md b/src/LiveDevelopment/MultiBrowserImpl/README.md index 76494953de8..f5dacd3629d 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/README.md +++ b/src/LiveDevelopment/MultiBrowserImpl/README.md @@ -1,26 +1,11 @@ This is an experimental implementation to replace the current live development architecture with something more flexible that isn't tied solely to Chrome Developer Tools. -It's based on the current Live Development code in Brackets, both implemenations can be swapped by setting 'livedev.impl' preference: - 'livedev.impl' : 'livedev2' - enable livedev2 experimental implementation (replaces current LiveDevelopment impl based on CDT) - 'livedev.impl' : 'default' (or not set) - disable livedev2 (keeps curren LiveDevelopment implementation) +It's based on the current Live Development code in Brackets and has been incubated at njx/brackets-livedev2. ### What's working -If enabled, it will launch will launch the page in your default browser when clicking on the LiveDevelopment icon as it works today. You should then also be able to copy and paste the URL from that browser into any other browser, live edits will then update all connected browsers at once. - -This is still an experimental implementation, the basic functionality for CSS/HTML editing is working but there are could be some scenarios that might be partially or entirely not covered yet. Page reload when saving changes on related Javascript files is also working. Documents that are loaded by the current HTML live doc are being tracked by ```DocumentObserver``` at the browser side which relies on DOM MutationObserver for monitoring added/removed stylesheets and Javascript files. - -### Bugs/cleanup/TODO - -* Doesn't show an error if the browser never connects back -* spurious errors when socket is closed -* hard-coded port number for WebSocket server (might be fine) -* It doesn't work on IE (need a fix on ```RemoteFunctions``` which has to be included in Brackets core - see #20) -* Lots of TODOs in the code - -#### Unit tests - -We would definitely need a good suite of unit tests for the new functionality. I suspect it would be easier to just write entirely new, more granular unit tests than to try to reuse the old LiveDevelopment integration tests (which were fragile anyway). +It can be enabled by setting ```livedev.multibrowser``` preference to true. If enabled, it will launch will launch the page in your default browser when clicking on the LiveDevelopment icon as it works today. You should then also be able to copy and paste the URL from that browser into any other browser, live edits will then update all connected browsers at once. +This is still an experimental implementation, the basic functionality for CSS/HTML editing is working but there are could be some scenarios that might be partially or entirely not covered yet. Page reload when saving changes on related Javascript files is also working. Documents that are loaded by the current HTML live doc are being tracked by ```DocumentObserver``` at the browser side which relies on DOM MutationObserver for monitoring added/removed stylesheets and Javascript files (just monitoring `````` and ```