From cc721d47d4fa745aa31e864190601c8cf8d4cbbc Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Wed, 29 Oct 2014 11:26:23 -0300 Subject: [PATCH 1/8] Move LiveDevelopment implementation to default In preparation for landing livedev2 as an alternative implementation, a folder /impls was created under LiveDevelopment folder and a the current implementation moved to a /default folder. Servers folder and LiveDevServerManager keep in their original location since they will be used as shared resources for both implementations. --- .../{ => impls/default}/Agents/CSSAgent.js | 2 +- .../default}/Agents/ConsoleAgent.js | 2 +- .../{ => impls/default}/Agents/DOMAgent.js | 8 ++--- .../{ => impls/default}/Agents/DOMHelpers.js | 0 .../{ => impls/default}/Agents/DOMNode.js | 2 +- .../{ => impls/default}/Agents/EditAgent.js | 8 ++--- .../{ => impls/default}/Agents/GotoAgent.js | 8 ++--- .../default}/Agents/HighlightAgent.js | 8 ++--- .../default}/Agents/NetworkAgent.js | 2 +- .../{ => impls/default}/Agents/RemoteAgent.js | 6 ++-- .../default}/Agents/RemoteFunctions.js | 0 .../{ => impls/default}/Agents/ScriptAgent.js | 4 +-- .../default}/Documents/CSSDocument.js | 6 ++-- .../Documents/CSSPreprocessorDocument.js | 4 +-- .../default}/Documents/HTMLDocument.js | 8 ++--- .../default}/Documents/JSDocument.js | 6 ++-- .../default}/Inspector/Inspector.js | 2 +- .../default}/Inspector/Inspector.json | 0 .../default}/Inspector/inspector.html | 0 .../{ => impls/default}/Inspector/jsdoc.rb | 0 .../{ => impls/default}/LiveDevelopment.js | 30 +++++++++---------- .../{ => impls/default}/launch.html | 0 .../{ => impls/default}/main.js | 6 ++-- .../{ => impls/default}/main.less | 0 src/brackets.js | 10 +++---- src/command/Commands.js | 6 ++-- src/document/DocumentCommandHandlers.js | 2 +- 27 files changed, 65 insertions(+), 65 deletions(-) rename src/LiveDevelopment/{ => impls/default}/Agents/CSSAgent.js (99%) rename src/LiveDevelopment/{ => impls/default}/Agents/ConsoleAgent.js (97%) rename src/LiveDevelopment/{ => impls/default}/Agents/DOMAgent.js (97%) rename src/LiveDevelopment/{ => impls/default}/Agents/DOMHelpers.js (100%) rename src/LiveDevelopment/{ => impls/default}/Agents/DOMNode.js (99%) rename src/LiveDevelopment/{ => impls/default}/Agents/EditAgent.js (93%) rename src/LiveDevelopment/{ => impls/default}/Agents/GotoAgent.js (95%) rename src/LiveDevelopment/{ => impls/default}/Agents/HighlightAgent.js (93%) rename src/LiveDevelopment/{ => impls/default}/Agents/NetworkAgent.js (97%) rename src/LiveDevelopment/{ => impls/default}/Agents/RemoteAgent.js (95%) rename src/LiveDevelopment/{ => impls/default}/Agents/RemoteFunctions.js (100%) rename src/LiveDevelopment/{ => impls/default}/Agents/ScriptAgent.js (97%) rename src/LiveDevelopment/{ => impls/default}/Documents/CSSDocument.js (97%) rename src/LiveDevelopment/{ => impls/default}/Documents/CSSPreprocessorDocument.js (96%) rename src/LiveDevelopment/{ => impls/default}/Documents/HTMLDocument.js (97%) rename src/LiveDevelopment/{ => impls/default}/Documents/JSDocument.js (95%) rename src/LiveDevelopment/{ => impls/default}/Inspector/Inspector.js (99%) rename src/LiveDevelopment/{ => impls/default}/Inspector/Inspector.json (100%) rename src/LiveDevelopment/{ => impls/default}/Inspector/inspector.html (100%) rename src/LiveDevelopment/{ => impls/default}/Inspector/jsdoc.rb (100%) rename src/LiveDevelopment/{ => impls/default}/LiveDevelopment.js (97%) rename src/LiveDevelopment/{ => impls/default}/launch.html (100%) rename src/LiveDevelopment/{ => impls/default}/main.js (97%) rename src/LiveDevelopment/{ => impls/default}/main.less (100%) 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..92340122790 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/main.js b/src/LiveDevelopment/impls/default/main.js similarity index 97% rename from src/LiveDevelopment/main.js rename to src/LiveDevelopment/impls/default/main.js index 0c8c7f68eb0..dfc6edb7d4c 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/impls/default/main.js @@ -39,8 +39,8 @@ define(function main(require, exports, module) { var DocumentManager = require("document/DocumentManager"), Commands = require("command/Commands"), AppInit = require("utils/AppInit"), - LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), + 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"), @@ -82,7 +82,7 @@ define(function main(require, exports, module) { /** Load Live Development LESS Style */ function _loadStyles() { - var lessText = require("text!LiveDevelopment/main.less"), + var lessText = require("text!LiveDevelopment/impls/default/main.less"), parser = new less.Parser(); parser.parse(lessText, function onParse(err, tree) { 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/brackets.js b/src/brackets.js index e0d5e728334..60d77651c11 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -103,7 +103,7 @@ define(function (require, exports, module) { require("file/FileUtils"); require("project/SidebarView"); require("utils/Resizer"); - require("LiveDevelopment/main"); + require("LiveDevelopment/impls/default/main"); require("utils/NodeConnection"); require("utils/NodeDomain"); require("utils/ColorUtils"); @@ -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"), From c99eb5562499222414179255c104c024d959cd70 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 3 Nov 2014 10:07:33 -0300 Subject: [PATCH 2/8] Align tests to new LiveDevelopment location Make tests run after moving LiveDevelopment to /impls/default --- test/spec/FileFilters-test.js | 2 +- test/spec/HTMLInstrumentation-test.js | 2 +- test/spec/LiveDevelopment-test.js | 16 ++++++++-------- test/spec/RemoteFunctions-test.js | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) 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() + ")()"); From 2915b1d574554bd693e5c8716ef8018a8dfc06d5 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 3 Nov 2014 17:13:06 -0300 Subject: [PATCH 3/8] Integrate livedev2 under LiveDevelopment/impls Copy files from njx/brackets-livedev2. Summary of changes to be integrated into brackets core: - require context, module loading, paths were aligned to the new location - preference names and main.js modified to work on top of current LiveDevelopment UI rather than adding a new launch icon - dependencies for NodeSocketTransportDomain were not included (need to run npm install to get it working) At this point, LiveDevelopment implementation can be manually switched by changing the module that is being load at brackets.js: /impls/default/main.js -> /impls/livedev2/main.js --- .../impls/livedev2/LiveDevelopment.js | 911 ++++++++++++++++++ .../livedev2/documents/LiveCSSDocument.js | 201 ++++ .../impls/livedev2/documents/LiveDocument.js | 341 +++++++ .../livedev2/documents/LiveHTMLDocument.js | 343 +++++++ .../livedev2/language/HTMLInstrumentation.js | 876 +++++++++++++++++ .../impls/livedev2/language/HTMLSimpleDOM.js | 558 +++++++++++ src/LiveDevelopment/impls/livedev2/main.js | 218 +++++ .../livedev2/protocol/LiveDevProtocol.js | 321 ++++++ .../protocol/remote/DocumentObserver.js | 309 ++++++ .../remote/ExtendedRemoteFunctions.js | 63 ++ .../protocol/remote/LiveDevProtocolRemote.js | 283 ++++++ .../images/live_development_sprites.svg | 27 + .../impls/livedev2/styles/styles.css | 41 + .../transports/NodeSocketTransport.js | 89 ++ .../node/NodeSocketTransportDomain.js | 249 +++++ .../livedev2/transports/node/package.json | 9 + .../remote/NodeSocketTransportRemote.js | 138 +++ 17 files changed, 4977 insertions(+) create mode 100644 src/LiveDevelopment/impls/livedev2/LiveDevelopment.js create mode 100644 src/LiveDevelopment/impls/livedev2/documents/LiveCSSDocument.js create mode 100644 src/LiveDevelopment/impls/livedev2/documents/LiveDocument.js create mode 100644 src/LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument.js create mode 100644 src/LiveDevelopment/impls/livedev2/language/HTMLInstrumentation.js create mode 100644 src/LiveDevelopment/impls/livedev2/language/HTMLSimpleDOM.js create mode 100644 src/LiveDevelopment/impls/livedev2/main.js create mode 100644 src/LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol.js create mode 100644 src/LiveDevelopment/impls/livedev2/protocol/remote/DocumentObserver.js create mode 100644 src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js create mode 100644 src/LiveDevelopment/impls/livedev2/protocol/remote/LiveDevProtocolRemote.js create mode 100644 src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg create mode 100644 src/LiveDevelopment/impls/livedev2/styles/styles.css create mode 100644 src/LiveDevelopment/impls/livedev2/transports/NodeSocketTransport.js create mode 100644 src/LiveDevelopment/impls/livedev2/transports/node/NodeSocketTransportDomain.js create mode 100644 src/LiveDevelopment/impls/livedev2/transports/node/package.json create mode 100644 src/LiveDevelopment/impls/livedev2/transports/remote/NodeSocketTransportRemote.js diff --git a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js new file mode 100644 index 00000000000..b7b4b24ef8c --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js @@ -0,0 +1,911 @@ +/* + * 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 + * + * (TODO: some of these are likely obsolete in the new architecture) + * Status updates are dispatched as `statusChange` jQuery events. The status + * is passed as the first parameter and the reason for the change as the second + * parameter. Currently only the "Inactive" status supports the reason parameter. + * The status codes are: + * + * -1: Error + * 0: Inactive + * 1: Connecting to the remote debugger + * 2: Loading agents + * 3: Active + * 4: Out of sync + * + * The reason codes are: + * - null (Unknown reason) + * - "explicit_close" (LiveDevelopment.close() was called) + * - "navigated_away" (The browser changed to a location outside of the project) + * - "detached_target_closed" (The tab or window was closed) + * - "detached_replaced_with_devtools" (The developer tools were opened in the browser) + */ +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. + * TODO: this is not yet maintained in the new architecture - will need to be reimplemented. + * @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 + * Get the current active document from the document manager. + * TODO: might no longer be necessary - there used to be more stuff in here but I think it was + * removed awhile ago. + * @return {?Document} The currently active document, or null for no document. + */ + function _getCurrentDocument() { + return DocumentManager.getCurrentDocument(); + } + + /** + * @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 undefined; + } + + 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 {$.Event} event + * @param {LiveDocument} liveDoc + */ + 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 = new DocClass(_protocol, _resolveUrl, doc, editor, roots); + + if (!DocClass) { + return null; + } + + $(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 docClass = _classForDocument(doc), + 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. + * TODO: this isn't implemented in the prototype yet. We'll need to implement + * this notification on the browser side. + * @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 = _getCurrentDocument(), + refPath, + i; + + // TODO: FileUtils.getParentFolder() + function getParentFolder(path) { + return path.substring(0, path.lastIndexOf('/', path.length - 2) + 1); + } + + function getFilenameWithoutExtension(filename) { + var index = filename.lastIndexOf("."); + return index === -1 ? filename : filename.slice(0, index); + } + + // 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 = getParentFolder(item.fullPath); + + return (containingFolder.indexOf(parent) === 0); + }); + + var filterIndexFile = function (fileInfo) { + if (fileInfo.fullPath.indexOf(containingFolder) === 0) { + if (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 = getParentFolder(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 the connection and the associated window asynchronously + * @return {jQuery.Promise} Resolves once the connection is closed + */ + 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 = _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("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. + * TODO: closing the current preview doesn't actually work in the new architecture. + */ + function _onDocumentChange() { + var doc = _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. + * TODO: not implemented, see below. + * @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); + } + } + + // TODO: These aren't necessary in the prototype because they're related to servers that are + // registered by the original LiveDevelopment feature when it starts up. +// function getCurrentProjectServerConfig() { +// return { +// baseUrl: ProjectManager.getBaseUrl(), +// pathResolver: ProjectManager.makeProjectRelativeIfPossible, +// root: ProjectManager.getProjectRoot().fullPath +// }; +// } +// +// function _createUserServer() { +// return new UserServer(getCurrentProjectServerConfig()); +// } +// +// function _createFileServer() { +// return new FileServer(getCurrentProjectServerConfig()); +// } + + /** + * 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. + * TODO: we should probably have a way of returning the results from all clients, not just the first? + */ + 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. + * TODO: we should probably have a way of returning the results from all clients, not just the first? + */ + 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..76cf2e2b305 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js @@ -0,0 +1,63 @@ +/* + * 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, 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/styles/images/live_development_sprites.svg b/src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg new file mode 100644 index 00000000000..7e1c824cf6b --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/LiveDevelopment/impls/livedev2/styles/styles.css b/src/LiveDevelopment/impls/livedev2/styles/styles.css new file mode 100644 index 00000000000..cfe7f8d26aa --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/styles/styles.css @@ -0,0 +1,41 @@ +/* + * 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. + * + */ + +#toolbar-go-live-new { + content: ""; + background: url("images/live_development_sprites.svg") 0 0 no-repeat; + width: 24px; + height: 24px; +} +#toolbar-go-live-new.success { + background-position: 0 -24px; +} +#toolbar-go-live-new.info { + background-position: 0 -48px; +} +#toolbar-go-live-new.out-of-sync { + background-position: 0 -72px; +} +#toolbar-go-live-new.sync-error { + background-position: 0 -96px; +} 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..6e4a682f0d6 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/transports/node/package.json @@ -0,0 +1,9 @@ +{ + "name": "brackets-livedev2-server", + "dependencies": { + "ws": "~0.4.31", + "connect": "~2.14.3", + "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)); From 5f902adf24b42ec5bbdd82b7234072461484aa07 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 3 Nov 2014 12:27:15 -0300 Subject: [PATCH 4/8] Add pref to allow switching LiveDevelopment impls Add livedev.impl preference to let the user switch between 'default' and 'livedev2' implementations. --- src/LiveDevelopment/impls/default/main.js | 5 +- src/LiveDevelopment/impls/livedev2/main.js | 7 ++- src/LiveDevelopment/main.js | 68 ++++++++++++++++++++++ src/brackets.js | 2 +- 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/LiveDevelopment/main.js diff --git a/src/LiveDevelopment/impls/default/main.js b/src/LiveDevelopment/impls/default/main.js index dfc6edb7d4c..3854e6b9a92 100644 --- a/src/LiveDevelopment/impls/default/main.js +++ b/src/LiveDevelopment/impls/default/main.js @@ -232,7 +232,7 @@ define(function main(require, exports, module) { } /** Initialize LiveDevelopment */ - AppInit.appReady(function () { + function init() { params.parse(); Inspector.init(config); @@ -261,7 +261,7 @@ define(function main(require, exports, module) { LiveDevelopment.redrawHighlight(); } }); - }); + } // init prefs PreferencesManager.stateManager.definePreference("livedev.highlight", "boolean", true) @@ -284,4 +284,5 @@ define(function main(require, exports, module) { CommandManager.get(Commands.FILE_LIVE_HIGHLIGHT).setEnabled(false); // Export public functions + exports.init = init; }); diff --git a/src/LiveDevelopment/impls/livedev2/main.js b/src/LiveDevelopment/impls/livedev2/main.js index d2ee97aaa22..735243110cb 100644 --- a/src/LiveDevelopment/impls/livedev2/main.js +++ b/src/LiveDevelopment/impls/livedev2/main.js @@ -189,7 +189,7 @@ define(function main(require, exports, module) { } /** Initialize LiveDevelopment */ - AppInit.appReady(function () { + function init() { params.parse(); LiveDevelopment.init(); @@ -197,7 +197,7 @@ define(function main(require, exports, module) { _setupGoLiveMenu(); _updateHighlightCheckmark(); - }); + } // init prefs PreferencesManager.stateManager.definePreference("file.previewHighlight", "boolean", true) @@ -215,4 +215,7 @@ define(function main(require, exports, module) { //Menus.getMenu(Menus.AppMenuBar.VIEW_MENU).addMenuItem("livedev2.live-highlight"); ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); + + // Export public functions + exports.init = init; }); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js new file mode 100644 index 00000000000..ba830a470f8 --- /dev/null +++ b/src/LiveDevelopment/main.js @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define */ + +/** + * 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 AppInit = require("utils/AppInit"), + PreferencesManager = require("preferences/PreferencesManager"); + + // pre-loaded implementations + var liveDevImpls = { + 'default' : require("LiveDevelopment/impls/default/main"), + 'livedev2' : require("LiveDevelopment/impls/livedev2/main") + }; + + // current active implementation + var LiveDevelopment; + + /** Initialize LiveDevelopment */ + AppInit.appReady(function () { + PreferencesManager.definePreference('livedev.impl', 'string', 'default'); + // get choose LiveDevelopment implementation based on preference value + LiveDevelopment = liveDevImpls[PreferencesManager.get('livedev.impl')]; + 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']; + } + // init + LiveDevelopment.init(); + }); +}); \ No newline at end of file diff --git a/src/brackets.js b/src/brackets.js index 60d77651c11..d96334ef714 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -103,7 +103,7 @@ define(function (require, exports, module) { require("file/FileUtils"); require("project/SidebarView"); require("utils/Resizer"); - require("LiveDevelopment/impls/default/main"); + require("LiveDevelopment/main"); require("utils/NodeConnection"); require("utils/NodeDomain"); require("utils/ColorUtils"); From 5b702d69badde80e345c7fc1f4c2dd4ac48301ff Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Wed, 5 Nov 2014 16:01:50 -0300 Subject: [PATCH 5/8] clean-up --- .../impls/livedev2/LiveDevelopment.js | 40 ++++-------------- .../livedev2/documents/LiveCSSDocument.js | 29 ------------- .../livedev2/documents/LiveHTMLDocument.js | 37 +---------------- src/LiveDevelopment/impls/livedev2/main.js | 7 ---- .../livedev2/protocol/LiveDevProtocol.js | 4 +- .../remote/ExtendedRemoteFunctions.js | 2 +- .../images/live_development_sprites.svg | 27 ------------ .../impls/livedev2/styles/styles.css | 41 ------------------- .../livedev2/transports/node/package.json | 1 - 9 files changed, 10 insertions(+), 178 deletions(-) delete mode 100644 src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg delete mode 100644 src/LiveDevelopment/impls/livedev2/styles/styles.css diff --git a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js index b7b4b24ef8c..b6f8e6e0553 100644 --- a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js +++ b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js @@ -48,10 +48,12 @@ * * -1: Error * 0: Inactive - * 1: Connecting to the remote debugger - * 2: Loading agents - * 3: Active - * 4: Out of sync + * 1: Connecting (waiting for a browser connection) + * 2: Active + * 3: Out of sync + * 4: Sync error + * 5: Reloading (JS changes) + * 6: Restarting (switching context to a new HTML live doc) * * The reason codes are: * - null (Unknown reason) @@ -104,7 +106,6 @@ define(function (require, exports, module) { * @private * Live documents related to the active HTML document - for example, CSS files * that are used by the document. - * TODO: this is not yet maintained in the new architecture - will need to be reimplemented. * @type {Object.} */ var _relatedDocuments = {}; @@ -325,8 +326,6 @@ define(function (require, exports, module) { * 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. - * TODO: this isn't implemented in the prototype yet. We'll need to implement - * this notification on the browser side. * @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 ) @@ -514,8 +513,7 @@ define(function (require, exports, module) { } /** - * Close the connection and the associated window asynchronously - * @return {jQuery.Promise} Resolves once the connection is closed + * Close all active connections */ function close() { return _close(true); @@ -803,24 +801,6 @@ define(function (require, exports, module) { } } - // TODO: These aren't necessary in the prototype because they're related to servers that are - // registered by the original LiveDevelopment feature when it starts up. -// function getCurrentProjectServerConfig() { -// return { -// baseUrl: ProjectManager.getBaseUrl(), -// pathResolver: ProjectManager.makeProjectRelativeIfPossible, -// root: ProjectManager.getProjectRoot().fullPath -// }; -// } -// -// function _createUserServer() { -// return new UserServer(getCurrentProjectServerConfig()); -// } -// -// function _createFileServer() { -// return new FileServer(getCurrentProjectServerConfig()); -// } - /** * Sets the current transport mechanism to be used by the live development protocol * (e.g. socket server, iframe postMessage, etc.) @@ -858,11 +838,6 @@ define(function (require, exports, module) { .on("dirtyFlagChange", _onDirtyFlagChange); $(ProjectManager).on("beforeProjectClose beforeAppClose", close); - // Register user defined server provider - // TODO: main LiveDevelopment does this already, so we don't want to do it again here. -// LiveDevServerManager.registerServer({ create: _createUserServer }, 99); -// LiveDevServerManager.registerServer({ create: _createFileServer }, 0); - // Default transport for live connection messages - can be changed setTransport(NodeSocketTransport); @@ -905,7 +880,6 @@ define(function (require, exports, module) { exports.getLiveDocForPath = getLiveDocForPath; exports.init = init; exports.isActive = isActive; -// exports.getCurrentProjectServerConfig = getCurrentProjectServerConfig; exports.getServerBaseUrl = getServerBaseUrl; exports.setTransport = setTransport; }); \ No newline at end of file diff --git a/src/LiveDevelopment/impls/livedev2/documents/LiveCSSDocument.js b/src/LiveDevelopment/impls/livedev2/documents/LiveCSSDocument.js index 83e688b9d06..1978f2b78f9 100644 --- a/src/LiveDevelopment/impls/livedev2/documents/LiveCSSDocument.js +++ b/src/LiveDevelopment/impls/livedev2/documents/LiveCSSDocument.js @@ -77,35 +77,6 @@ define(function LiveCSSDocumentModule(require, exports, module) { LiveCSSDocument.prototype = Object.create(LiveDocument.prototype); LiveCSSDocument.prototype.constructor = LiveCSSDocument; LiveCSSDocument.prototype.parentClass = LiveDocument.prototype; - - /** - * @private - * Returns information about the associated style block in the browser, including a - * unique ID. - */ - LiveCSSDocument.prototype._getStyleSheetHeader = function () { - // TODO Need to add protocol API for getting a stylesheet ID, or - // decide to just refer to them by URL. - //return CSSAgent.styleForURL(this.doc.url); - }; - - /** - * Get the browser version of the source - * @return {jQuery.promise} Promise resolved with the text content of this CSS document - */ - LiveCSSDocument.prototype.getSourceFromBrowser = function () { - // TODO: Only used for unit testing. Need to add protocol API to extract stylesheet from browser side. -// var deferred = new $.Deferred(), -// styleSheetId = this._getStyleSheetHeader().styleSheetId, -// inspectorPromise = Inspector.CSS.getStyleSheetText(styleSheetId); -// -// inspectorPromise.then(function (res) { -// deferred.resolve(res.text); -// }, deferred.reject); -// -// return deferred.promise(); - - }; /** * @override diff --git a/src/LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument.js b/src/LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument.js index 09313125ee2..a05d7064085 100644 --- a/src/LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument.js +++ b/src/LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument.js @@ -180,42 +180,7 @@ define(function (require, exports, module) { * @param {Object} change CodeMirror editor change data */ LiveHTMLDocument.prototype._compareWithBrowser = function (change) { - var self = this; - - // TODO: evaluate in browser -// RemoteAgent.call("getSimpleDOM").done(function (res) { -// var browserSimpleDOM = JSON.parse(res.result.value), -// edits, -// node, -// result; -// -// try { -// result = HTMLInstrumentation._getBrowserDiff(self.editor, browserSimpleDOM); -// } catch (err) { -// console.error("Error comparing in-browser DOM to in-editor DOM"); -// console.error(err.stack); -// return; -// } -// -// edits = result.diff.filter(function (delta) { -// // ignore textDelete in html root element -// node = result.browser.nodeMap[delta.parentID]; -// -// if (node && node.tag === "html" && delta.type === "textDelete") { -// return false; -// } -// -// return true; -// }); -// -// if (edits.length > 0) { -// console.warn("Browser DOM does not match after change: " + JSON.stringify(change)); -// -// edits.forEach(function (delta) { -// console.log(delta); -// }); -// } -// }); + // TODO: Not implemented. }; /** diff --git a/src/LiveDevelopment/impls/livedev2/main.js b/src/LiveDevelopment/impls/livedev2/main.js index 735243110cb..3c257d85c31 100644 --- a/src/LiveDevelopment/impls/livedev2/main.js +++ b/src/LiveDevelopment/impls/livedev2/main.js @@ -175,7 +175,6 @@ define(function main(require, exports, module) { $(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("livedev2.live-preview").setChecked(status === LiveDevelopment.STATUS_ACTIVE); CommandManager.get("file.previewHighlight").setEnabled(status === LiveDevelopment.STATUS_INACTIVE); }); } @@ -208,13 +207,7 @@ define(function main(require, exports, module) { // 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); - - //Menus.getMenu(Menus.AppMenuBar.FILE_MENU).addMenuItem("livedev2.live-preview"); - //Menus.getMenu(Menus.AppMenuBar.VIEW_MENU).addMenuItem("livedev2.live-highlight"); - - ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); // Export public functions exports.init = init; diff --git a/src/LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol.js b/src/LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol.js index 2e3827121de..ed0df3c5585 100644 --- a/src/LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol.js @@ -92,6 +92,7 @@ define(function (require, exports, module) { * If the message has an `id` field, it's assumed to be a response to a previous * request, and will be passed along to the original promise returned by `_send()`. * Otherwise, it's treated as an event and dispatched. + * TODO: we should probably have a way of returning the results from all clients, not just the first? * * @param {number} clientId ID of the client that sent the message * @param {string} msg The message that was sent, in JSON string format @@ -239,7 +240,6 @@ define(function (require, exports, module) { * @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. - * TODO: we should probably have a way of returning the results from all clients, not just the first? */ function evaluate(script, clients) { return _send( @@ -259,7 +259,6 @@ define(function (require, exports, module) { * @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. - * TODO: we should probably have a way of returning the results from all clients, not just the first? */ function reload(ignoreCache, clients) { return _send( @@ -307,7 +306,6 @@ define(function (require, exports, module) { _connections = {}; } - // public API exports.setTransport = setTransport; exports.getRemoteScript = getRemoteScript; diff --git a/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js b/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js index 76cf2e2b305..8ca1671595b 100644 --- a/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js +++ b/src/LiveDevelopment/impls/livedev2/protocol/remote/ExtendedRemoteFunctions.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * 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"), diff --git a/src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg b/src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg deleted file mode 100644 index 7e1c824cf6b..00000000000 --- a/src/LiveDevelopment/impls/livedev2/styles/images/live_development_sprites.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/LiveDevelopment/impls/livedev2/styles/styles.css b/src/LiveDevelopment/impls/livedev2/styles/styles.css deleted file mode 100644 index cfe7f8d26aa..00000000000 --- a/src/LiveDevelopment/impls/livedev2/styles/styles.css +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - -#toolbar-go-live-new { - content: ""; - background: url("images/live_development_sprites.svg") 0 0 no-repeat; - width: 24px; - height: 24px; -} -#toolbar-go-live-new.success { - background-position: 0 -24px; -} -#toolbar-go-live-new.info { - background-position: 0 -48px; -} -#toolbar-go-live-new.out-of-sync { - background-position: 0 -72px; -} -#toolbar-go-live-new.sync-error { - background-position: 0 -96px; -} diff --git a/src/LiveDevelopment/impls/livedev2/transports/node/package.json b/src/LiveDevelopment/impls/livedev2/transports/node/package.json index 6e4a682f0d6..17e04631800 100644 --- a/src/LiveDevelopment/impls/livedev2/transports/node/package.json +++ b/src/LiveDevelopment/impls/livedev2/transports/node/package.json @@ -2,7 +2,6 @@ "name": "brackets-livedev2-server", "dependencies": { "ws": "~0.4.31", - "connect": "~2.14.3", "open": "0.0.4", "lodash": "~2.4.1" } From 7b21f1c8f6cad25598ae649922bf41d632c7e808 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Thu, 6 Nov 2014 11:03:01 -0300 Subject: [PATCH 6/8] code improvements, clean-up after initial review --- .../impls/default/LiveDevelopment.js | 8 +- .../impls/livedev2/LiveDevelopment.js | 93 ++++++------------- src/LiveDevelopment/main.js | 40 +++++--- 3 files changed, 59 insertions(+), 82 deletions(-) diff --git a/src/LiveDevelopment/impls/default/LiveDevelopment.js b/src/LiveDevelopment/impls/default/LiveDevelopment.js index 92340122790..4841eca3021 100644 --- a/src/LiveDevelopment/impls/default/LiveDevelopment.js +++ b/src/LiveDevelopment/impls/default/LiveDevelopment.js @@ -94,13 +94,13 @@ define(function LiveDevelopment(require, exports, module) { UserServer = require("LiveDevelopment/Servers/UserServer").UserServer; // Inspector - var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); + var Inspector = require("LiveDevelopment/impls/default/Inspector/Inspector"); // Documents - var CSSDocument = require("LiveDevelopment/impls/default/Documents/CSSDocument"), + 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"); + HTMLDocument = require("LiveDevelopment/impls/default/Documents/HTMLDocument"), + JSDocument = require("LiveDevelopment/impls/default/Documents/JSDocument"); // Document errors var SYNC_ERROR_CLASS = "live-preview-sync-error"; diff --git a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js index b6f8e6e0553..17563c18b61 100644 --- a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js +++ b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js @@ -66,14 +66,14 @@ 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 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"), @@ -92,8 +92,8 @@ define(function (require, exports, module) { LiveDevProtocol = require("LiveDevelopment/impls/livedev2/protocol/LiveDevProtocol"); // Documents - var LiveCSSDocument = require("LiveDevelopment/impls/livedev2/documents/LiveCSSDocument"), - LiveHTMLDocument = require("LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument"); + var LiveCSSDocument = require("LiveDevelopment/impls/livedev2/documents/LiveCSSDocument"), + LiveHTMLDocument = require("LiveDevelopment/impls/livedev2/documents/LiveHTMLDocument"); /** * @private @@ -141,17 +141,6 @@ define(function (require, exports, module) { (ProjectManager.getBaseUrl() && FileUtils.isServerHtmlFileExt(ext))); } - /** - * @private - * Get the current active document from the document manager. - * TODO: might no longer be necessary - there used to be more stuff in here but I think it was - * removed awhile ago. - * @return {?Document} The currently active document, or null for no document. - */ - function _getCurrentDocument() { - return DocumentManager.getCurrentDocument(); - } - /** * @private * Determine which live document class should be used for a given document @@ -185,7 +174,7 @@ define(function (require, exports, module) { */ function getLiveDocForPath(path) { if (!_server) { - return undefined; + return null; } return _server.get(path); @@ -217,8 +206,7 @@ define(function (require, exports, module) { * 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 {$.Event} event - * @param {LiveDocument} liveDoc + * @param {string} url Absolute URL of the related document */ function _handleRelatedDocumentDeleted(url) { var liveDoc = _relatedDocuments[url]; @@ -229,7 +217,6 @@ define(function (require, exports, module) { if (_server) { _server.remove(liveDoc); } - _closeDocument(liveDoc); } @@ -292,12 +279,14 @@ define(function (require, exports, module) { * @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 = new DocClass(_protocol, _resolveUrl, 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()) { @@ -315,8 +304,7 @@ define(function (require, exports, module) { * @return {boolean} */ function _docIsOutOfSync(doc) { - var docClass = _classForDocument(doc), - liveDoc = _server && _server.get(doc.file.fullPath), + var liveDoc = _server && _server.get(doc.file.fullPath), isLiveEditingEnabled = liveDoc && liveDoc.isLiveEditingEnabled(); return doc.isDirty && !isLiveEditingEnabled; @@ -394,20 +382,10 @@ define(function (require, exports, module) { * file. */ function _getInitialDocFromCurrent() { - var doc = _getCurrentDocument(), + var doc = DocumentManager.getCurrentDocument(), refPath, i; - // TODO: FileUtils.getParentFolder() - function getParentFolder(path) { - return path.substring(0, path.lastIndexOf('/', path.length - 2) + 1); - } - - function getFilenameWithoutExtension(filename) { - var index = filename.lastIndexOf("."); - return index === -1 ? filename : filename.slice(0, index); - } - // Is the currently opened document already a file we can use for Live Development? if (doc) { refPath = doc.file.fullPath; @@ -434,14 +412,14 @@ define(function (require, exports, module) { } var filteredFiltered = allFiles.filter(function (item) { - var parent = getParentFolder(item.fullPath); + var parent = FileUtils.getDirectoryPath(item.fullPath); return (containingFolder.indexOf(parent) === 0); }); var filterIndexFile = function (fileInfo) { if (fileInfo.fullPath.indexOf(containingFolder) === 0) { - if (getFilenameWithoutExtension(fileInfo.name) === "index") { + if (FileUtils.getFilenameWithoutExtension(fileInfo.name) === "index") { if (hasOwnServerForLiveDevelopment) { if ((FileUtils.isServerHtmlFileExt(fileInfo.name)) || (FileUtils.isStaticHtmlFileExt(fileInfo.name))) { @@ -462,7 +440,7 @@ define(function (require, exports, module) { // We found no good match if (i === -1) { // traverse the directory tree up one level - containingFolder = getParentFolder(containingFolder); + containingFolder = FileUtils.getDirectoryPath(containingFolder); // Are we still inside the project? if (containingFolder.indexOf(projectRoot) === -1) { stillInProjectTree = false; @@ -580,7 +558,7 @@ define(function (require, exports, module) { .on("Connection.connect.livedev", function (event, msg) { // check for the first connection if (_protocol.getConnectionIds().length === 1) { - var doc = _getCurrentDocument(); + 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); @@ -694,10 +672,9 @@ define(function (require, exports, module) { /** * @private * When switching documents, close the current preview and open a new one. - * TODO: closing the current preview doesn't actually work in the new architecture. */ function _onDocumentChange() { - var doc = _getCurrentDocument(); + var doc = DocumentManager.getCurrentDocument(); if (!isActive() || !doc) { return; } @@ -709,7 +686,6 @@ define(function (require, exports, module) { if (_liveDocument.doc.url !== docUrl && isViewable) { // clear live doc and related docs _closeDocuments(); - // create new live doc _createLiveDocumentForFrame(doc); _setStatus(STATUS_RESTARTING); @@ -753,7 +729,6 @@ define(function (require, exports, module) { /** * 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. - * TODO: not implemented, see below. * @param {$.Event} event * @param {Document} doc */ @@ -845,16 +820,6 @@ define(function (require, exports, module) { _setStatus(STATUS_INACTIVE); } - /** - * @private - * Returns the current server being used to serve the active live document. Will be null - * if there is no active live document. - * @return {?BaseServer} - */ - function _getServer() { - return _server; - } - /** * @private * Returns the base URL of the current server serving the active live document, or null if @@ -865,14 +830,10 @@ define(function (require, exports, module) { return _server && _server.getBaseUrl(); } - function _getCurrentLiveDoc() { - return _liveDocument; - } - // For unit testing - exports._getServer = _getServer; + exports._server = _server; + exports._liveDocument = _liveDocument; exports._getInitialDocFromCurrent = _getInitialDocFromCurrent; - exports._getCurrentLiveDoc = _getCurrentLiveDoc; // Export public functions exports.open = open; @@ -882,4 +843,4 @@ define(function (require, exports, module) { exports.isActive = isActive; exports.getServerBaseUrl = getServerBaseUrl; exports.setTransport = setTransport; -}); \ No newline at end of file +}); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index ba830a470f8..2e338dcff2c 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -40,29 +40,45 @@ define(function main(require, exports, module) { "use strict"; - var AppInit = require("utils/AppInit"), - PreferencesManager = require("preferences/PreferencesManager"); + // preference to set the implementation to be loaded + var LIVEDEV_IMPL_PREF = 'livedev.impl'; - // pre-loaded implementations - var liveDevImpls = { - 'default' : require("LiveDevelopment/impls/default/main"), - 'livedev2' : require("LiveDevelopment/impls/livedev2/main") - }; + // pre-defined implementations + var DEFAULT_IMPL = 'default', + LIVEDEV2_IMPL = 'livedev2'; - // current active implementation + var AppInit = require("utils/AppInit"), + PreferencesManager = require("preferences/PreferencesManager"); + + /** + * current active implementation + */ var LiveDevelopment; + + // pre-load implementations + var liveDevImpls = {}; + liveDevImpls[DEFAULT_IMPL] = require("LiveDevelopment/impls/default/main"); + liveDevImpls[LIVEDEV2_IMPL] = require("LiveDevelopment/impls/livedev2/main"); + + // define livedev.impl preference + PreferencesManager.definePreference(LIVEDEV_IMPL_PREF, 'string', 'default'); /** Initialize LiveDevelopment */ AppInit.appReady(function () { - PreferencesManager.definePreference('livedev.impl', 'string', 'default'); + // get choose LiveDevelopment implementation based on preference value - LiveDevelopment = liveDevImpls[PreferencesManager.get('livedev.impl')]; + 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']; + LiveDevelopment = liveDevImpls[DEFAULT_IMPL]; } // init LiveDevelopment.init(); }); -}); \ No newline at end of file + + // exports public API + exports.LIVEDEV_IMPL_PREF = LIVEDEV_IMPL_PREF; + exports.DEFAULT_IMPL = DEFAULT_IMPL; + exports.LIVEDEV2_IMPL = LIVEDEV2_IMPL; +}); From 8edfe19e95403cecffe8f943bdb130915aa18b20 Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 10 Nov 2014 11:19:30 -0300 Subject: [PATCH 7/8] Make detached_target_closed reason be displayed It now shows the twipsy that informs the user when the session has finished because of the browser/tab has been closed. --- src/LiveDevelopment/impls/livedev2/LiveDevelopment.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js index 17563c18b61..76516b44b98 100644 --- a/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js +++ b/src/LiveDevelopment/impls/livedev2/LiveDevelopment.js @@ -40,7 +40,6 @@ * * # STATUS * - * (TODO: some of these are likely obsolete in the new architecture) * 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. @@ -52,7 +51,7 @@ * 2: Active * 3: Out of sync * 4: Sync error - * 5: Reloading (JS changes) + * 5: Reloading (after saving JS changes) * 6: Restarting (switching context to a new HTML live doc) * * The reason codes are: @@ -60,7 +59,6 @@ * - "explicit_close" (LiveDevelopment.close() was called) * - "navigated_away" (The browser changed to a location outside of the project) * - "detached_target_closed" (The tab or window was closed) - * - "detached_replaced_with_devtools" (The developer tools were opened in the browser) */ define(function (require, exports, module) { "use strict"; @@ -569,7 +567,7 @@ define(function (require, exports, module) { // close session when the last connection was closed if (_protocol.getConnectionIds().length === 0) { if (exports.status <= STATUS_ACTIVE) { - close("detached_target_closed"); + _close(false, "detached_target_closed"); } } }) From 87644ed7c29528060ed288c4f04fcc12e1e0945e Mon Sep 17 00:00:00 2001 From: Sebastian Salvucci Date: Mon, 10 Nov 2014 12:30:01 -0300 Subject: [PATCH 8/8] add README file --- src/LiveDevelopment/impls/livedev2/README.md | 83 ++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/LiveDevelopment/impls/livedev2/README.md diff --git a/src/LiveDevelopment/impls/livedev2/README.md b/src/LiveDevelopment/impls/livedev2/README.md new file mode 100644 index 00000000000..76494953de8 --- /dev/null +++ b/src/LiveDevelopment/impls/livedev2/README.md @@ -0,0 +1,83 @@ +This is an experimental implementation to replace the current live development architecture with something more flexible that isn't tied solely to Chrome Developer Tools. +It's based on the current Live Development code in Brackets, both implemenations can be swapped by setting 'livedev.impl' preference: + 'livedev.impl' : 'livedev2' - enable livedev2 experimental implementation (replaces current LiveDevelopment impl based on CDT) + 'livedev.impl' : 'default' (or not set) - disable livedev2 (keeps curren LiveDevelopment implementation) + +### What's working + +If enabled, it will launch will launch the page in your default browser when clicking on the LiveDevelopment icon as it works today. You should then also be able to copy and paste the URL from that browser into any other browser, live edits will then update all connected browsers at once. + +This is still an experimental implementation, the basic functionality for CSS/HTML editing is working but there are could be some scenarios that might be partially or entirely not covered yet. Page reload when saving changes on related Javascript files is also working. Documents that are loaded by the current HTML live doc are being tracked by ```DocumentObserver``` at the browser side which relies on DOM MutationObserver for monitoring added/removed stylesheets and Javascript files. + +### Bugs/cleanup/TODO + +* Doesn't show an error if the browser never connects back +* spurious errors when socket is closed +* hard-coded port number for WebSocket server (might be fine) +* It doesn't work on IE (need a fix on ```RemoteFunctions``` which has to be included in Brackets core - see #20) +* Lots of TODOs in the code + +#### Unit tests + +We would definitely need a good suite of unit tests for the new functionality. I suspect it would be easier to just write entirely new, more granular unit tests than to try to reuse the old LiveDevelopment integration tests (which were fragile anyway). + + +### Basic architecture + +The primary difference in this architecture is that communication with the browser is done via an injected script rather than CDT's native remote debugging interface, and the browser connects back to Brackets rather than Brackets connecting to the browser. This makes it so: + +* launching a preview, injecting scripts into the HTML, and establishing the connection between the previewed page and Brackets are relatively simple and largely decoupled +* live preview can work in any browser, not just Chrome +* multiple browsers can connect to the same live preview session in Brackets +* browsers could theoretically connect from anywhere on the network that can see Brackets (though right now it's only implemented for localhost) +* opening dev tools in the browser doesn't break live preview + +Communication between Brackets and the browser is factored into three layers: + +1. a low-level "transport" layer, which is responsible for launching live preview in the browser and providing a simple textual message bus between the browser and Brackets. +2. the "protocol" layer, which sits on top of the transport layer and provides the actual semantic behavior (currently just "evaluate in browser") +3. the injected RemoteFunctions script, which is the same as in today's LiveDevelopment and provides Brackets-specific functionality (highlighting, DOM edit application) on top of the core protocol. + +The reason for this factoring is so that the transport layer can be swapped out for different use cases, and so that anything higher-level we need that can be easily built in terms of eval doesn't have to be built into the protocol. + +(We could arguably get rid of the distinction between (2) and (3), and basically roll all the Brackets functionality into the "protocol" layer by simply merging the RemoteFunctions script into the protocol remote script. The only reason to keep the protocol layer separate, IMO, is if we want to keep it compatible with CDT, a la RemoteDebug - so it only provides the functionality that CDT does.) + +The transport layer currently implemented uses a WebSocket server in Node, coupled with an injected script in the browser that connects back to that server. However, this could easily be swapped out for a different transport layer that supports a preview iframe directly inside Brackets, where the communication is via `postMessage()`. + +The protocol layer currently exposes a very simple API that currently just contains specific protocol functions: + * "evaluate" which evals in the browser + * "reload" which reload the page in the browser + * "navigate" which allow the browser navigate to a given URL + +The over-the-wire protocol is a JSON message that more or less looks like the CDT wire protocol, although it's not an exact match right now - again, we could decide to make it exactly mimic CDT if we wanted. + +If we want to eventually reintroduce a CDT connection (or hook up to RemoteDebug), we have two choices: we could either just implement it as a separate transport, or we could implement it as a separate protocol impl entirely. Implementing it as a transport would be easier, and would be fine for talking to our own injected script; but it would only make sense for talking to CDT-specific functionality if we were very good about our wire protocol looking like the CDT wire protocol in general. Otherwise, we would probably want to consider swapping out the protocol entirely. + +### Explanation of the flow + +I've created a [really crappy block diagram](https://raw.githubusercontent.com/wiki/njx/brackets-livedev2/livedev2-block-diagram.png) of how the various bits talk to each other. + +Here's a short summary of what happens when the user clicks on the Live Preview button on an HTML page. + +1. LiveDevelopment creates a LiveHTMLDocument for the page, passing it the protocol handler (LiveDevProtocol). LiveHTMLDocument manages communication between the editor and the browser for HTML pages. +2. LiveDevelopment tells StaticServer that this path has a live document. StaticServer is in charge of actually serving the page and associated assets to the browser. (Note: eventually I think we should get rid of this step - StaticServer shouldn't know anything about live documents directly; it should just have a way of request instrumented text for HTML URLs.) +3. LiveDevelopment tells the protocol to open the page via the StaticServer URL. The protocol just passes this through to the transport (NodeSocketTransport), which first creates a WebSocket server if it hasn't already, then opens the page in the default browser. +4. The browser requests the page from StaticServer. StaticServer notes that there is a live document for this page, and requests an instrumented version of the page from LiveHTMLDocument. (The current "requestFilterPaths" mechanism for this could be simplified, I think.) +5. LiveHTMLDocument instruments the page for live editing using the existing HTMLInstrumentation mechanism, and additionally includes remote scripts provided by the protocol (LiveDevProtocolRemote), transport (NodeSocketTransportRemote), remote functions (RemoteFunctions) and document observation (DocumentObserver) which tracks related documents. The transport script includes the URL for the WebSocket server created in step 3. +6. The instrumented page is sent back to StaticServer, which responds to the browser with the instrumented version. Other files requested by the browser are simply returned directly by StaticServer. +7. As the browser loads the page, it encounters the injected scripts. The transport script connects back to the NodeSocketTransport's WebSocket server created in step 3 and sends it a "connect" message to tell it what URL has been loaded in the browser. The NodeSocketTransport assigns the socket a client id so it can keep track of which socket is associated with which page instance, then raises a "connect" event. +8. LivDevProtocol receives the "connect" event and makes a note of the associated client ID updating the current active connections. LiveDevelopment also receives the "connect" event, it checks that the URL that comes on the message matches the current live doc instance and updates session status to STATUS_ACTIVE in case this is the first client connected. +After the live document is loaded in the browser, DocumentObserver scan related stylesheets and Javascript files and send a message 'Ducment.Related' which contains their URLs. LiveDevelopment receives the message, extract stylesshes and creates a LiveCSSDocument instance per each of them. LiveHTMLDocument listen to the same message and further notifications (added/removed) to keep info about its related docs which is needed by LiveDocument (eg. when saving changes of a JS file). +9. As the user makes live edits or changes selection, LiveHTMLDocument calls the protocol handler's "evaluate" function to call functions from the injected RemoteFunctions. +10. The protocol's "evaluate" method packages up the request as a JSON message and sends it via the transport. +11. The remote transport handler unpacks the message and passes it to the remote protocol handler, which finally interprets it and evals its content. +12. If another browser loads the same page (from the StaticServer URL), steps 4-8 repeat, with LiveHTMLDocument just adding the new connection's client ID to its list. Future evals are then sent to all the associated client IDs for the page. + + +### Changes from existing LiveDevelopment code + +* the existing code for talking to Chrome Developer Tools via the remote debugging interface is gone for now +* CSSDocument and HTMLDocument were renamed to LiveCSSDocument and LiveHTMLDocument, with a new LiveDocument base class +* the "agents" are all gone - a lot of them were dead code anyway; other functionality was rolled into LiveDocument +* communication is factored into transport and protocol layers (see above) +* HTMLInstrumentation and HTMLSimpleDOM were modified slightly (which is why they're copied into the extension), to make it possible to inject the remote scripts and to fix an issue with re-instrumenting the HTML when a second browser connects to Live Development. The former change is harmless; the latter change would need some review or possibly more work in order to merge into master.