diff --git a/src/LiveDevelopment/Agents/CSSAgent.js b/src/LiveDevelopment/impls/default/Agents/CSSAgent.js similarity index 99% rename from src/LiveDevelopment/Agents/CSSAgent.js rename to src/LiveDevelopment/impls/default/Agents/CSSAgent.js index eb5dd98a94a..9e306692b43 100644 --- a/src/LiveDevelopment/Agents/CSSAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/CSSAgent.js @@ -39,7 +39,7 @@ define(function CSSAgent(require, exports, module) { var _ = require("thirdparty/lodash"); - var Inspector = require("LiveDevelopment/Inspector/Inspector"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); /** * Stylesheet details diff --git a/src/LiveDevelopment/Agents/ConsoleAgent.js b/src/LiveDevelopment/impls/default/Agents/ConsoleAgent.js similarity index 97% rename from src/LiveDevelopment/Agents/ConsoleAgent.js rename to src/LiveDevelopment/impls/default/Agents/ConsoleAgent.js index e0d479cc294..40db86612f4 100644 --- a/src/LiveDevelopment/Agents/ConsoleAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/ConsoleAgent.js @@ -32,7 +32,7 @@ define(function ConsoleAgent(require, exports, module) { "use strict"; - var Inspector = require("LiveDevelopment/Inspector/Inspector"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); var _lastMessage; // {Console.ConsoleMessage} the last received message diff --git a/src/LiveDevelopment/Agents/DOMAgent.js b/src/LiveDevelopment/impls/default/Agents/DOMAgent.js similarity index 97% rename from src/LiveDevelopment/Agents/DOMAgent.js rename to src/LiveDevelopment/impls/default/Agents/DOMAgent.js index f27ec6f621c..f55ee5e9fcc 100644 --- a/src/LiveDevelopment/Agents/DOMAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/DOMAgent.js @@ -40,10 +40,10 @@ define(function DOMAgent(require, exports, module) { var $exports = $(exports); - var Inspector = require("LiveDevelopment/Inspector/Inspector"); - var EditAgent = require("LiveDevelopment/Agents/EditAgent"); - var DOMNode = require("LiveDevelopment/Agents/DOMNode"); - var DOMHelpers = require("LiveDevelopment/Agents/DOMHelpers"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); + var EditAgent = require("LiveDevelopment/impls/default/Agents/EditAgent"); + var DOMNode = require("LiveDevelopment/impls/default/Agents/DOMNode"); + var DOMHelpers = require("LiveDevelopment/impls/default/Agents/DOMHelpers"); var _load; // {$.Deferred} load promise var _idToNode; // {nodeId -> node} diff --git a/src/LiveDevelopment/Agents/DOMHelpers.js b/src/LiveDevelopment/impls/default/Agents/DOMHelpers.js similarity index 100% rename from src/LiveDevelopment/Agents/DOMHelpers.js rename to src/LiveDevelopment/impls/default/Agents/DOMHelpers.js diff --git a/src/LiveDevelopment/Agents/DOMNode.js b/src/LiveDevelopment/impls/default/Agents/DOMNode.js similarity index 99% rename from src/LiveDevelopment/Agents/DOMNode.js rename to src/LiveDevelopment/impls/default/Agents/DOMNode.js index 31927e6c135..682fcf7e9a5 100644 --- a/src/LiveDevelopment/Agents/DOMNode.js +++ b/src/LiveDevelopment/impls/default/Agents/DOMNode.js @@ -35,7 +35,7 @@ define(function DOMNodeModule(require, exports, module) { "use strict"; - var DOMHelpers = require("LiveDevelopment/Agents/DOMHelpers"); + var DOMHelpers = require("LiveDevelopment/impls/default/Agents/DOMHelpers"); /** Fill a string to the given length (used for debug output) * @param {string} source string diff --git a/src/LiveDevelopment/Agents/EditAgent.js b/src/LiveDevelopment/impls/default/Agents/EditAgent.js similarity index 93% rename from src/LiveDevelopment/Agents/EditAgent.js rename to src/LiveDevelopment/impls/default/Agents/EditAgent.js index 5d13d9fa602..96e9ee44dd9 100644 --- a/src/LiveDevelopment/Agents/EditAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/EditAgent.js @@ -32,10 +32,10 @@ define(function EditAgent(require, exports, module) { "use strict"; - var Inspector = require("LiveDevelopment/Inspector/Inspector"); - var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); - var RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); - var GotoAgent = require("LiveDevelopment/Agents/GotoAgent"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); + var DOMAgent = require("LiveDevelopment/impls/default/Agents/DOMAgent"); + var RemoteAgent = require("LiveDevelopment/impls/default/Agents/RemoteAgent"); + var GotoAgent = require("LiveDevelopment/impls/default/Agents/GotoAgent"); var EditorManager = require("editor/EditorManager"); diff --git a/src/LiveDevelopment/Agents/GotoAgent.js b/src/LiveDevelopment/impls/default/Agents/GotoAgent.js similarity index 95% rename from src/LiveDevelopment/Agents/GotoAgent.js rename to src/LiveDevelopment/impls/default/Agents/GotoAgent.js index 9107a86d279..d94746fc587 100644 --- a/src/LiveDevelopment/Agents/GotoAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/GotoAgent.js @@ -33,10 +33,10 @@ define(function GotoAgent(require, exports, module) { require("utils/Global"); - var Inspector = require("LiveDevelopment/Inspector/Inspector"), - DOMAgent = require("LiveDevelopment/Agents/DOMAgent"), - ScriptAgent = require("LiveDevelopment/Agents/ScriptAgent"), - RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"), + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), + DOMAgent = require("LiveDevelopment/impls/default/Agents/DOMAgent"), + ScriptAgent = require("LiveDevelopment/impls/default/Agents/ScriptAgent"), + RemoteAgent = require("LiveDevelopment/impls/default/Agents/RemoteAgent"), EditorManager = require("editor/EditorManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"); diff --git a/src/LiveDevelopment/Agents/HighlightAgent.js b/src/LiveDevelopment/impls/default/Agents/HighlightAgent.js similarity index 93% rename from src/LiveDevelopment/Agents/HighlightAgent.js rename to src/LiveDevelopment/impls/default/Agents/HighlightAgent.js index 08f978b6365..280d5a44226 100644 --- a/src/LiveDevelopment/Agents/HighlightAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/HighlightAgent.js @@ -34,10 +34,10 @@ define(function HighlightAgent(require, exports, module) { "use strict"; - var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), - LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), - RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"), + var DOMAgent = require("LiveDevelopment/impls/default/Agents/DOMAgent"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), + LiveDevelopment = require("LiveDevelopment/impls/default/LiveDevelopment"), + RemoteAgent = require("LiveDevelopment/impls/default/Agents/RemoteAgent"), _ = require("thirdparty/lodash"); var _highlight = {}; // active highlight diff --git a/src/LiveDevelopment/Agents/NetworkAgent.js b/src/LiveDevelopment/impls/default/Agents/NetworkAgent.js similarity index 97% rename from src/LiveDevelopment/Agents/NetworkAgent.js rename to src/LiveDevelopment/impls/default/Agents/NetworkAgent.js index db1fa022d71..be11ade2351 100644 --- a/src/LiveDevelopment/Agents/NetworkAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/NetworkAgent.js @@ -32,7 +32,7 @@ define(function NetworkAgent(require, exports, module) { "use strict"; - var Inspector = require("LiveDevelopment/Inspector/Inspector"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); var _urlRequested = {}; // url -> request info diff --git a/src/LiveDevelopment/Agents/RemoteAgent.js b/src/LiveDevelopment/impls/default/Agents/RemoteAgent.js similarity index 95% rename from src/LiveDevelopment/Agents/RemoteAgent.js rename to src/LiveDevelopment/impls/default/Agents/RemoteAgent.js index 031768969e4..19796d2323b 100644 --- a/src/LiveDevelopment/Agents/RemoteAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/RemoteAgent.js @@ -37,9 +37,9 @@ define(function RemoteAgent(require, exports, module) { var $exports = $(exports); - var LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), - RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"); + var LiveDevelopment = require("LiveDevelopment/impls/default/LiveDevelopment"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), + RemoteFunctions = require("text!LiveDevelopment/impls/default/Agents/RemoteFunctions.js"); var _load; // deferred load var _objectId; // the object id of the remote object diff --git a/src/LiveDevelopment/Agents/RemoteFunctions.js b/src/LiveDevelopment/impls/default/Agents/RemoteFunctions.js similarity index 100% rename from src/LiveDevelopment/Agents/RemoteFunctions.js rename to src/LiveDevelopment/impls/default/Agents/RemoteFunctions.js diff --git a/src/LiveDevelopment/Agents/ScriptAgent.js b/src/LiveDevelopment/impls/default/Agents/ScriptAgent.js similarity index 97% rename from src/LiveDevelopment/Agents/ScriptAgent.js rename to src/LiveDevelopment/impls/default/Agents/ScriptAgent.js index 26604535d7d..7b64e7c6b04 100644 --- a/src/LiveDevelopment/Agents/ScriptAgent.js +++ b/src/LiveDevelopment/impls/default/Agents/ScriptAgent.js @@ -32,8 +32,8 @@ define(function ScriptAgent(require, exports, module) { "use strict"; - var Inspector = require("LiveDevelopment/Inspector/Inspector"); - var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); + var DOMAgent = require("LiveDevelopment/impls/default/Agents/DOMAgent"); var _load; // the load promise var _urlToScript; // url -> script info diff --git a/src/LiveDevelopment/Documents/CSSDocument.js b/src/LiveDevelopment/impls/default/Documents/CSSDocument.js similarity index 97% rename from src/LiveDevelopment/Documents/CSSDocument.js rename to src/LiveDevelopment/impls/default/Documents/CSSDocument.js index 86a1ca6095b..35dd55d8a3e 100644 --- a/src/LiveDevelopment/Documents/CSSDocument.js +++ b/src/LiveDevelopment/impls/default/Documents/CSSDocument.js @@ -50,11 +50,11 @@ define(function CSSDocumentModule(require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"), - CSSAgent = require("LiveDevelopment/Agents/CSSAgent"), + CSSAgent = require("LiveDevelopment/impls/default/Agents/CSSAgent"), CSSUtils = require("language/CSSUtils"), EditorManager = require("editor/EditorManager"), - HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"), - Inspector = require("LiveDevelopment/Inspector/Inspector"); + HighlightAgent = require("LiveDevelopment/impls/default/Agents/HighlightAgent"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); /** * @constructor diff --git a/src/LiveDevelopment/Documents/CSSPreprocessorDocument.js b/src/LiveDevelopment/impls/default/Documents/CSSPreprocessorDocument.js similarity index 96% rename from src/LiveDevelopment/Documents/CSSPreprocessorDocument.js rename to src/LiveDevelopment/impls/default/Documents/CSSPreprocessorDocument.js index 5c74ed48cea..0aeae0a674a 100644 --- a/src/LiveDevelopment/Documents/CSSPreprocessorDocument.js +++ b/src/LiveDevelopment/impls/default/Documents/CSSPreprocessorDocument.js @@ -40,8 +40,8 @@ define(function CSSPreprocessorDocumentModule(require, exports, module) { var _ = require("thirdparty/lodash"), CSSUtils = require("language/CSSUtils"), EditorManager = require("editor/EditorManager"), - HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"), - Inspector = require("LiveDevelopment/Inspector/Inspector"); + HighlightAgent = require("LiveDevelopment/impls/default/Agents/HighlightAgent"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); /** * @constructor diff --git a/src/LiveDevelopment/Documents/HTMLDocument.js b/src/LiveDevelopment/impls/default/Documents/HTMLDocument.js similarity index 97% rename from src/LiveDevelopment/Documents/HTMLDocument.js rename to src/LiveDevelopment/impls/default/Documents/HTMLDocument.js index 9ced712d038..8b279a51305 100644 --- a/src/LiveDevelopment/Documents/HTMLDocument.js +++ b/src/LiveDevelopment/impls/default/Documents/HTMLDocument.js @@ -45,12 +45,12 @@ define(function HTMLDocumentModule(require, exports, module) { "use strict"; var EditorManager = require("editor/EditorManager"), - HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"), + HighlightAgent = require("LiveDevelopment/impls/default/Agents/HighlightAgent"), HTMLInstrumentation = require("language/HTMLInstrumentation"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), - LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), + LiveDevelopment = require("LiveDevelopment/impls/default/LiveDevelopment"), PerfUtils = require("utils/PerfUtils"), - RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"), + RemoteAgent = require("LiveDevelopment/impls/default/Agents/RemoteAgent"), _ = require("thirdparty/lodash"); /** diff --git a/src/LiveDevelopment/Documents/JSDocument.js b/src/LiveDevelopment/impls/default/Documents/JSDocument.js similarity index 95% rename from src/LiveDevelopment/Documents/JSDocument.js rename to src/LiveDevelopment/impls/default/Documents/JSDocument.js index 715bba53a89..c04ae4b9c08 100644 --- a/src/LiveDevelopment/Documents/JSDocument.js +++ b/src/LiveDevelopment/impls/default/Documents/JSDocument.js @@ -45,9 +45,9 @@ define(function JSDocumentModule(require, exports, module) { "use strict"; - var Inspector = require("LiveDevelopment/Inspector/Inspector"); - var ScriptAgent = require("LiveDevelopment/Agents/ScriptAgent"); - var HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); + var ScriptAgent = require("LiveDevelopment/impls/default/Agents/ScriptAgent"); + var HighlightAgent = require("LiveDevelopment/impls/default/Agents/HighlightAgent"); /** * @constructor diff --git a/src/LiveDevelopment/Inspector/Inspector.js b/src/LiveDevelopment/impls/default/Inspector/Inspector.js similarity index 99% rename from src/LiveDevelopment/Inspector/Inspector.js rename to src/LiveDevelopment/impls/default/Inspector/Inspector.js index fa36e1bb974..39818db8ab3 100644 --- a/src/LiveDevelopment/Inspector/Inspector.js +++ b/src/LiveDevelopment/impls/default/Inspector/Inspector.js @@ -392,7 +392,7 @@ define(function Inspector(require, exports, module) { function init(theConfig) { exports.config = theConfig; - var InspectorText = require("text!LiveDevelopment/Inspector/Inspector.json"), + var InspectorText = require("text!LiveDevelopment/impls/default/Inspector/Inspector.json"), InspectorJSON = JSON.parse(InspectorText); var i, j, domain, command; diff --git a/src/LiveDevelopment/Inspector/Inspector.json b/src/LiveDevelopment/impls/default/Inspector/Inspector.json similarity index 100% rename from src/LiveDevelopment/Inspector/Inspector.json rename to src/LiveDevelopment/impls/default/Inspector/Inspector.json diff --git a/src/LiveDevelopment/Inspector/inspector.html b/src/LiveDevelopment/impls/default/Inspector/inspector.html similarity index 100% rename from src/LiveDevelopment/Inspector/inspector.html rename to src/LiveDevelopment/impls/default/Inspector/inspector.html diff --git a/src/LiveDevelopment/Inspector/jsdoc.rb b/src/LiveDevelopment/impls/default/Inspector/jsdoc.rb similarity index 100% rename from src/LiveDevelopment/Inspector/jsdoc.rb rename to src/LiveDevelopment/impls/default/Inspector/jsdoc.rb diff --git a/src/LiveDevelopment/LiveDevelopment.js b/src/LiveDevelopment/impls/default/LiveDevelopment.js similarity index 97% rename from src/LiveDevelopment/LiveDevelopment.js rename to src/LiveDevelopment/impls/default/LiveDevelopment.js index 342ba5fb853..4841eca3021 100644 --- a/src/LiveDevelopment/LiveDevelopment.js +++ b/src/LiveDevelopment/impls/default/LiveDevelopment.js @@ -94,30 +94,30 @@ define(function LiveDevelopment(require, exports, module) { UserServer = require("LiveDevelopment/Servers/UserServer").UserServer; // Inspector - var Inspector = require("LiveDevelopment/Inspector/Inspector"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); // Documents - var CSSDocument = require("LiveDevelopment/Documents/CSSDocument"), - CSSPreprocessorDocument = require("LiveDevelopment/Documents/CSSPreprocessorDocument"), - HTMLDocument = require("LiveDevelopment/Documents/HTMLDocument"), - JSDocument = require("LiveDevelopment/Documents/JSDocument"); + var CSSDocument = require("LiveDevelopment/impls/default/Documents/CSSDocument"), + CSSPreprocessorDocument = require("LiveDevelopment/impls/default/Documents/CSSPreprocessorDocument"), + HTMLDocument = require("LiveDevelopment/impls/default/Documents/HTMLDocument"), + JSDocument = require("LiveDevelopment/impls/default/Documents/JSDocument"); // Document errors var SYNC_ERROR_CLASS = "live-preview-sync-error"; // Agents - var CSSAgent = require("LiveDevelopment/Agents/CSSAgent"); + var CSSAgent = require("LiveDevelopment/impls/default/Agents/CSSAgent"); var agents = { - "console" : require("LiveDevelopment/Agents/ConsoleAgent"), - "remote" : require("LiveDevelopment/Agents/RemoteAgent"), - "network" : require("LiveDevelopment/Agents/NetworkAgent"), - "dom" : require("LiveDevelopment/Agents/DOMAgent"), + "console" : require("LiveDevelopment/impls/default/Agents/ConsoleAgent"), + "remote" : require("LiveDevelopment/impls/default/Agents/RemoteAgent"), + "network" : require("LiveDevelopment/impls/default/Agents/NetworkAgent"), + "dom" : require("LiveDevelopment/impls/default/Agents/DOMAgent"), "css" : CSSAgent, - "script" : require("LiveDevelopment/Agents/ScriptAgent"), - "highlight" : require("LiveDevelopment/Agents/HighlightAgent"), - "goto" : require("LiveDevelopment/Agents/GotoAgent"), - "edit" : require("LiveDevelopment/Agents/EditAgent") + "script" : require("LiveDevelopment/impls/default/Agents/ScriptAgent"), + "highlight" : require("LiveDevelopment/impls/default/Agents/HighlightAgent"), + "goto" : require("LiveDevelopment/impls/default/Agents/GotoAgent"), + "edit" : require("LiveDevelopment/impls/default/Agents/EditAgent") }; // construct path to launch.html @@ -133,7 +133,7 @@ define(function LiveDevelopment(require, exports, module) { // baseUrl is configured dynamically launcherUrl = launcherUrl.replace("/test/SpecRunner.html", "/src/index.html"); - launcherUrl = launcherUrl.substr(0, launcherUrl.lastIndexOf("/")) + "/LiveDevelopment/launch.html"; + launcherUrl = launcherUrl.substr(0, launcherUrl.lastIndexOf("/")) + "/LiveDevelopment/impls/default/launch.html"; launcherUrl = window.location.origin + launcherUrl; // Some agents are still experimental, so we don't enable them all by default diff --git a/src/LiveDevelopment/launch.html b/src/LiveDevelopment/impls/default/launch.html similarity index 100% rename from src/LiveDevelopment/launch.html rename to src/LiveDevelopment/impls/default/launch.html diff --git a/src/LiveDevelopment/impls/default/main.js b/src/LiveDevelopment/impls/default/main.js new file mode 100644 index 00000000000..3854e6b9a92 --- /dev/null +++ b/src/LiveDevelopment/impls/default/main.js @@ -0,0 +1,288 @@ +/* + * 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, $, 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 + */ +define(function main(require, exports, module) { + "use strict"; + + var DocumentManager = require("document/DocumentManager"), + Commands = require("command/Commands"), + AppInit = require("utils/AppInit"), + LiveDevelopment = require("LiveDevelopment/impls/default/LiveDevelopment"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), + 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"); + + var params = new UrlParams(); + var config = { + experimental: false, // enable experimental features + debug: true, // enable debug output and helpers + autoconnect: false, // go live automatically after startup? + highlight: true, // enable highlighting? + highlightConfig: { // the highlight configuration for the Inspector + borderColor: {r: 255, g: 229, b: 153, a: 0.66}, + contentColor: {r: 111, g: 168, b: 220, a: 0.55}, + marginColor: {r: 246, g: 178, b: 107, a: 0.66}, + paddingColor: {r: 147, g: 196, b: 125, a: 0.66}, + showInfo: true + } + }; + // 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 _$btnGoLive; // reference to the GoLive button + + /** Load Live Development LESS Style */ + function _loadStyles() { + var lessText = require("text!LiveDevelopment/impls/default/main.less"), + parser = new less.Parser(); + + parser.parse(lessText, function onParse(err, tree) { + console.assert(!err, err); + ExtensionUtils.addEmbeddedStyleSheet(tree.toCSS()); + }); + } + + /** + * 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("livedev.afterFirstLaunch")) { + PreferencesManager.setViewState("livedev.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 = $("#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); + if (config.autoconnect) { + window.sessionStorage.setItem("live.enabled", status === 3); + } + }); + + // 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(Commands.FILE_LIVE_FILE_PREVIEW).setChecked(status === LiveDevelopment.STATUS_ACTIVE); + CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(status === LiveDevelopment.STATUS_ACTIVE); + }); + } + + function _updateHighlightCheckmark() { + CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setChecked(config.highlight); + } + + function _handlePreviewHighlightCommand() { + config.highlight = !config.highlight; + _updateHighlightCheckmark(); + if (config.highlight) { + LiveDevelopment.showHighlight(); + } else { + LiveDevelopment.hideHighlight(); + } + PreferencesManager.setViewState("livedev.highlight", config.highlight); + } + + /** Setup window references to useful LiveDevelopment modules */ + function _setupDebugHelpers() { + window.ld = LiveDevelopment; + window.i = Inspector; + window.report = function report(params) { window.params = params; console.info(params); }; + } + + /** force reload the live preview */ + function _handleReloadLivePreviewCommand() { + if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { + LiveDevelopment.reload(); + } + } + + /** Initialize LiveDevelopment */ + function init() { + params.parse(); + + Inspector.init(config); + LiveDevelopment.init(config); + _loadStyles(); + _setupGoLiveButton(); + _setupGoLiveMenu(); + + _updateHighlightCheckmark(); + + if (config.debug) { + _setupDebugHelpers(); + } + + // trigger autoconnect + if (config.autoconnect && + window.sessionStorage.getItem("live.enabled") === "true" && + DocumentManager.getCurrentDocument()) { + _handleGoLiveCommand(); + } + + // Redraw highlights when window gets focus. This ensures that the highlights + // will be in sync with any DOM changes that may have occurred. + $(window).focus(function () { + if (Inspector.connected() && config.highlight) { + LiveDevelopment.redrawHighlight(); + } + }); + } + + // init prefs + PreferencesManager.stateManager.definePreference("livedev.highlight", "boolean", true) + .on("change", function () { + config.highlight = PreferencesManager.getViewState("livedev.highlight"); + _updateHighlightCheckmark(); + }); + + PreferencesManager.convertPreferences(module, { + "highlight": "user livedev.highlight", + "afterFirstLaunch": "user livedev.afterFirstLaunch" + }, true); + + config.highlight = PreferencesManager.getViewState("livedev.highlight"); + + // 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.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand); + CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); + + // Export public functions + exports.init = init; +}); diff --git a/src/LiveDevelopment/main.less b/src/LiveDevelopment/impls/default/main.less similarity index 100% rename from src/LiveDevelopment/main.less rename to src/LiveDevelopment/impls/default/main.less diff --git a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js new file mode 100644 index 00000000000..76516b44b98 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.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/impls/livedev2/transports/NodeSocketTransport"), + LiveDevProtocol = require("LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol"); + + // Documents + var LiveCSSDocument = require("LiveDevelopment/impls/livedev2/documents/LiveCSSDocument"), + LiveHTMLDocument = require("LiveDevelopment/impls/livedev2/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/impls/livedev2/protocol/remote/DocumentObserver.js b/src/LiveDevelopment/impls/livedev2/protocol/remote/DocumentObserver.js new file mode 100644 index 00000000000..254c6e3d7e6 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js b/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js new file mode 100644 index 00000000000..8ca1671595b --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/protocol/remote/LiveDevProtocolRemote.js b/src/LiveDevelopment/impls/livedev2/protocol/remote/LiveDevProtocolRemote.js new file mode 100644 index 00000000000..b7a5e08fae1 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/transports/NodeSocketTransport.js b/src/LiveDevelopment/impls/livedev2/transports/NodeSocketTransport.js new file mode 100644 index 00000000000..3d7329e32c6 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/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/impls/livedev2/transports/node/NodeSocketTransportDomain.js b/src/LiveDevelopment/impls/livedev2/transports/node/NodeSocketTransportDomain.js new file mode 100644 index 00000000000..33c22c78cba --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/transports/node/package.json b/src/LiveDevelopment/impls/livedev2/transports/node/package.json new file mode 100644 index 00000000000..17e04631800 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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/impls/livedev2/transports/remote/NodeSocketTransportRemote.js b/src/LiveDevelopment/impls/livedev2/transports/remote/NodeSocketTransportRemote.js new file mode 100644 index 00000000000..02b2dacad50 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/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..2e338dcff2c 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * 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"), @@ -21,267 +21,64 @@ * */ - -/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ -/*global define, $, less, window */ +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define */ /** - * 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 + * main loads a LiveDevelopment implementation: + * + * LiveDevelopment implementation its being set by 'livedev.impl' preference. + * There are currently two alternative values: + * + * 'default' : current default implementation based on CDT + * 'livedev2' : experimental implementation (CDT-independent) + * + * See impls/livedev2/README.md for more details on livedev2 implemetantion. + * */ + define(function main(require, exports, module) { "use strict"; - - var DocumentManager = require("document/DocumentManager"), - Commands = require("command/Commands"), - AppInit = require("utils/AppInit"), - LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), - 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"); - - var params = new UrlParams(); - var config = { - experimental: false, // enable experimental features - debug: true, // enable debug output and helpers - autoconnect: false, // go live automatically after startup? - highlight: true, // enable highlighting? - highlightConfig: { // the highlight configuration for the Inspector - borderColor: {r: 255, g: 229, b: 153, a: 0.66}, - contentColor: {r: 111, g: 168, b: 220, a: 0.55}, - marginColor: {r: 246, g: 178, b: 107, a: 0.66}, - paddingColor: {r: 147, g: 196, b: 125, a: 0.66}, - showInfo: true - } - }; - // 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 _$btnGoLive; // reference to the GoLive button - - /** Load Live Development LESS Style */ - function _loadStyles() { - var lessText = require("text!LiveDevelopment/main.less"), - parser = new less.Parser(); + + // preference to set the implementation to be loaded + var LIVEDEV_IMPL_PREF = 'livedev.impl'; - parser.parse(lessText, function onParse(err, tree) { - console.assert(!err, err); - ExtensionUtils.addEmbeddedStyleSheet(tree.toCSS()); - }); - } - - /** - * 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); - } - } - + // pre-defined implementations + var DEFAULT_IMPL = 'default', + LIVEDEV2_IMPL = 'livedev2'; + + var AppInit = require("utils/AppInit"), + PreferencesManager = require("preferences/PreferencesManager"); + /** - * 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). + * current active implementation */ - function _handleGoLiveCommand() { - if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { - LiveDevelopment.close(); - } else if (LiveDevelopment.status <= LiveDevelopment.STATUS_INACTIVE) { - if (!params.get("skipLiveDevelopmentInfo") && !PreferencesManager.getViewState("livedev.afterFirstLaunch")) { - PreferencesManager.setViewState("livedev.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); - } + var LiveDevelopment; - // 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 = $("#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); - if (config.autoconnect) { - window.sessionStorage.setItem("live.enabled", status === 3); - } - }); - - // 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(Commands.FILE_LIVE_FILE_PREVIEW).setChecked(status === LiveDevelopment.STATUS_ACTIVE); - CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(status === LiveDevelopment.STATUS_ACTIVE); - }); - } - - function _updateHighlightCheckmark() { - CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setChecked(config.highlight); - } - - function _handlePreviewHighlightCommand() { - config.highlight = !config.highlight; - _updateHighlightCheckmark(); - if (config.highlight) { - LiveDevelopment.showHighlight(); - } else { - LiveDevelopment.hideHighlight(); - } - PreferencesManager.setViewState("livedev.highlight", config.highlight); - } + // pre-load implementations + var liveDevImpls = {}; + liveDevImpls[DEFAULT_IMPL] = require("LiveDevelopment/impls/default/main"); + liveDevImpls[LIVEDEV2_IMPL] = require("LiveDevelopment/impls/livedev2/main"); - /** Setup window references to useful LiveDevelopment modules */ - function _setupDebugHelpers() { - window.ld = LiveDevelopment; - window.i = Inspector; - window.report = function report(params) { window.params = params; console.info(params); }; - } - - /** force reload the live preview */ - function _handleReloadLivePreviewCommand() { - if (LiveDevelopment.status >= LiveDevelopment.STATUS_ACTIVE) { - LiveDevelopment.reload(); - } - } + // define livedev.impl preference + PreferencesManager.definePreference(LIVEDEV_IMPL_PREF, 'string', 'default'); /** Initialize LiveDevelopment */ AppInit.appReady(function () { - params.parse(); - - Inspector.init(config); - LiveDevelopment.init(config); - _loadStyles(); - _setupGoLiveButton(); - _setupGoLiveMenu(); - - _updateHighlightCheckmark(); - if (config.debug) { - _setupDebugHelpers(); + // get choose LiveDevelopment implementation based on preference value + LiveDevelopment = liveDevImpls[PreferencesManager.get(LIVEDEV_IMPL_PREF)]; + if (!LiveDevelopment) { + // preference value doesn't match any implementation, switching to 'default' + console.log("invalid livedev.impl value - switching to default implemenation"); + LiveDevelopment = liveDevImpls[DEFAULT_IMPL]; } - - // trigger autoconnect - if (config.autoconnect && - window.sessionStorage.getItem("live.enabled") === "true" && - DocumentManager.getCurrentDocument()) { - _handleGoLiveCommand(); - } - - // Redraw highlights when window gets focus. This ensures that the highlights - // will be in sync with any DOM changes that may have occurred. - $(window).focus(function () { - if (Inspector.connected() && config.highlight) { - LiveDevelopment.redrawHighlight(); - } - }); + // init + LiveDevelopment.init(); }); - // init prefs - PreferencesManager.stateManager.definePreference("livedev.highlight", "boolean", true) - .on("change", function () { - config.highlight = PreferencesManager.getViewState("livedev.highlight"); - _updateHighlightCheckmark(); - }); - - PreferencesManager.convertPreferences(module, { - "highlight": "user livedev.highlight", - "afterFirstLaunch": "user livedev.afterFirstLaunch" - }, true); - - config.highlight = PreferencesManager.getViewState("livedev.highlight"); - - // 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.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand); - CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); - - // Export public functions + // exports public API + exports.LIVEDEV_IMPL_PREF = LIVEDEV_IMPL_PREF; + exports.DEFAULT_IMPL = DEFAULT_IMPL; + exports.LIVEDEV2_IMPL = LIVEDEV2_IMPL; }); diff --git a/src/brackets.js b/src/brackets.js index e0d5e728334..d96334ef714 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -171,7 +171,7 @@ define(function (require, exports, module) { DocumentCommandHandlers : DocumentCommandHandlers, DocumentManager : DocumentManager, DocumentModule : require("document/Document"), - DOMAgent : require("LiveDevelopment/Agents/DOMAgent"), + DOMAgent : require("LiveDevelopment/impls/default/Agents/DOMAgent"), DragAndDrop : DragAndDrop, EditorManager : EditorManager, ExtensionLoader : ExtensionLoader, @@ -185,12 +185,12 @@ define(function (require, exports, module) { FindInFiles : require("search/FindInFiles"), FindInFilesUI : require("search/FindInFilesUI"), HTMLInstrumentation : require("language/HTMLInstrumentation"), - Inspector : require("LiveDevelopment/Inspector/Inspector"), + Inspector : require("LiveDevelopment/impls/default/Inspector/Inspector"), InstallExtensionDialog : require("extensibility/InstallExtensionDialog"), JSUtils : JSUtils, KeyBindingManager : KeyBindingManager, LanguageManager : LanguageManager, - LiveDevelopment : require("LiveDevelopment/LiveDevelopment"), + LiveDevelopment : require("LiveDevelopment/impls/default/LiveDevelopment"), LiveDevServerManager : require("LiveDevelopment/LiveDevServerManager"), MainViewManager : MainViewManager, MainViewFactory : require("view/MainViewFactory"), @@ -200,7 +200,7 @@ define(function (require, exports, module) { PerfUtils : PerfUtils, PreferencesManager : PreferencesManager, ProjectManager : ProjectManager, - RemoteAgent : require("LiveDevelopment/Agents/RemoteAgent"), + RemoteAgent : require("LiveDevelopment/impls/default/Agents/RemoteAgent"), ScrollTrackMarkers : require("search/ScrollTrackMarkers"), UpdateNotification : require("utils/UpdateNotification"), WorkingSetView : WorkingSetView, diff --git a/src/command/Commands.js b/src/command/Commands.js index 87d0466caec..f736f9b7253 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -47,9 +47,9 @@ define(function (require, exports, module) { exports.FILE_CLOSE_ALL = "file.close_all"; // DocumentCommandHandlers.js handleFileCloseAll() exports.FILE_CLOSE_LIST = "file.close_list"; // DocumentCommandHandlers.js handleFileCloseList() exports.FILE_OPEN_DROPPED_FILES = "file.openDroppedFiles"; // DragAndDrop.js openDroppedFiles() - exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview"; // LiveDevelopment/main.js _handleGoLiveCommand() - exports.CMD_RELOAD_LIVE_PREVIEW = "file.reloadLivePreview"; // LiveDevelopment/main.js _handleReloadLivePreviewCommand() - exports.FILE_LIVE_HIGHLIGHT = "file.previewHighlight"; // LiveDevelopment/main.js _handlePreviewHighlightCommand() + exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview"; // LiveDevelopment/impls/default/main.js _handleGoLiveCommand() + exports.CMD_RELOAD_LIVE_PREVIEW = "file.reloadLivePreview"; // LiveDevelopment/impls/default/main.js _handleReloadLivePreviewCommand() + exports.FILE_LIVE_HIGHLIGHT = "file.previewHighlight"; // LiveDevelopment/impls/default/main.js _handlePreviewHighlightCommand() exports.FILE_PROJECT_SETTINGS = "file.projectSettings"; // ProjectManager.js _projectSettings() exports.FILE_RENAME = "file.rename"; // DocumentCommandHandlers.js handleFileRename() exports.FILE_DELETE = "file.delete"; // DocumentCommandHandlers.js handleFileDelete() diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 25f1fff634f..dad5217a267 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -53,7 +53,7 @@ define(function (require, exports, module) { PreferencesManager = require("preferences/PreferencesManager"), PerfUtils = require("utils/PerfUtils"), KeyEvent = require("utils/KeyEvent"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), + Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"), Menus = require("command/Menus"), UrlParams = require("utils/UrlParams").UrlParams, StatusBar = require("widgets/StatusBar"), diff --git a/test/spec/FileFilters-test.js b/test/spec/FileFilters-test.js index b4241ca7b61..97e47d1891f 100644 --- a/test/spec/FileFilters-test.js +++ b/test/spec/FileFilters-test.js @@ -396,7 +396,7 @@ define(function (require, exports, module) { expectEquivalent("node_*", "**node_***"); expectEquivalent("node_?", "**node_?**"); expectEquivalent("*-test-files/", "***-test-files/**"); - expectEquivalent("LiveDevelopment/**/Inspector", "**LiveDevelopment/**/Inspector**"); + expectEquivalent("LiveDevelopment/impls/default/**/Inspector", "**LiveDevelopment/impls/default/**/Inspector**"); }); it("shouldn't add ** suffix", function () { diff --git a/test/spec/HTMLInstrumentation-test.js b/test/spec/HTMLInstrumentation-test.js index 380d457c6f3..0f583bb0935 100644 --- a/test/spec/HTMLInstrumentation-test.js +++ b/test/spec/HTMLInstrumentation-test.js @@ -32,7 +32,7 @@ define(function (require, exports, module) { // Load dependent modules var HTMLInstrumentation = require("language/HTMLInstrumentation"), HTMLSimpleDOM = require("language/HTMLSimpleDOM"), - RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"), + RemoteFunctions = require("text!LiveDevelopment/impls/default/Agents/RemoteFunctions.js"), SpecRunnerUtils = require("spec/SpecRunnerUtils"), WellFormedDoc = require("text!spec/HTMLInstrumentation-test-files/wellformed.html"), NotWellFormedDoc = require("text!spec/HTMLInstrumentation-test-files/omitEndTags.html"), diff --git a/test/spec/LiveDevelopment-test.js b/test/spec/LiveDevelopment-test.js index 97ae366250f..e637fe4518a 100644 --- a/test/spec/LiveDevelopment-test.js +++ b/test/spec/LiveDevelopment-test.js @@ -50,18 +50,18 @@ define(function (require, exports, module) { ProjectManager; // Used as mocks - require("LiveDevelopment/main"); + require("LiveDevelopment/impls/default/main"); var CommandsModule = require("command/Commands"), CommandsManagerModule = require("command/CommandManager"), - LiveDevelopmentModule = require("LiveDevelopment/LiveDevelopment"), - InspectorModule = require("LiveDevelopment/Inspector/Inspector"), - CSSDocumentModule = require("LiveDevelopment/Documents/CSSDocument"), - CSSAgentModule = require("LiveDevelopment/Agents/CSSAgent"), - HighlightAgentModule = require("LiveDevelopment/Agents/HighlightAgent"), - HTMLDocumentModule = require("LiveDevelopment/Documents/HTMLDocument"), + LiveDevelopmentModule = require("LiveDevelopment/impls/default/LiveDevelopment"), + InspectorModule = require("LiveDevelopment/impls/default/Inspector/Inspector"), + CSSDocumentModule = require("LiveDevelopment/impls/default/Documents/CSSDocument"), + CSSAgentModule = require("LiveDevelopment/impls/default/Agents/CSSAgent"), + HighlightAgentModule = require("LiveDevelopment/impls/default/Agents/HighlightAgent"), + HTMLDocumentModule = require("LiveDevelopment/impls/default/Documents/HTMLDocument"), HTMLInstrumentationModule = require("language/HTMLInstrumentation"), NativeAppModule = require("utils/NativeApp"), - CSSPreprocessorDocumentModule = require("LiveDevelopment/Documents/CSSPreprocessorDocument"); + CSSPreprocessorDocumentModule = require("LiveDevelopment/impls/default/Documents/CSSPreprocessorDocument"); var testPath = SpecRunnerUtils.getTestPath("/spec/LiveDevelopment-test-files"), tempDir = SpecRunnerUtils.getTempDirectory(), diff --git a/test/spec/RemoteFunctions-test.js b/test/spec/RemoteFunctions-test.js index ae3ba0fd1c3..fd1c47253aa 100644 --- a/test/spec/RemoteFunctions-test.js +++ b/test/spec/RemoteFunctions-test.js @@ -27,7 +27,7 @@ define(function (require, exports, module) { 'use strict'; - var RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"); + var RemoteFunctions = require("text!LiveDevelopment/impls/default/Agents/RemoteFunctions.js"); // "load" RemoteFunctions RemoteFunctions = eval("(" + RemoteFunctions.trim() + ")()");