diff --git a/Gruntfile.js b/Gruntfile.js index 25c14983dc0..9f56ba54111 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -69,6 +69,7 @@ module.exports = function (grunt) { 'dependencies.js', 'thirdparty/requirejs/require.js', 'LiveDevelopment/launch.html', + 'LiveDevelopment/transports/**', 'LiveDevelopment/MultiBrowserImpl/transports/**', 'LiveDevelopment/MultiBrowserImpl/launchers/**' ] diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 468bde46e56..ac6be64c3b3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -62,6 +62,11 @@ "from": "chokidar@1.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.6.0.tgz" }, + "commander": { + "version": "2.1.0", + "from": "commander@>=2.1.0 <2.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz" + }, "concat-map": { "version": "0.0.1", "from": "concat-map@0.0.1", @@ -207,6 +212,11 @@ "from": "minimatch@>=3.0.2 <4.0.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz" }, + "nan": { + "version": "1.0.0", + "from": "nan@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.0.0.tgz" + }, "normalize-path": { "version": "2.0.1", "from": "normalize-path@>=2.0.1 <3.0.0", @@ -217,6 +227,11 @@ "from": "object.omit@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz" }, + "options": { + "version": "0.0.6", + "from": "options@>=0.0.5", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" + }, "parse-glob": { "version": "3.0.4", "from": "parse-glob@>=3.0.4 <4.0.0", @@ -277,10 +292,20 @@ "from": "string_decoder@>=0.10.0 <0.11.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, + "tinycolor": { + "version": "0.0.1", + "from": "tinycolor@>=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz" + }, "util-deprecate": { "version": "1.0.2", "from": "util-deprecate@>=1.0.1 <1.1.0", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "ws": { + "version": "0.4.32", + "from": "ws@>=0.4.31 <0.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz" } } } diff --git a/package.json b/package.json index 6f05d07cd2e..39825cd1b5a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "dependencies": { "anymatch": "1.3.0", "chokidar": "1.6.0", - "lodash": "4.15.0" + "lodash": "4.15.0", + "ws": "~0.4.31" }, "devDependencies": { "glob": "7.0.6", diff --git a/src/LiveDevelopment/Agents/RemoteAgent.js b/src/LiveDevelopment/Agents/RemoteAgent.js index 855b19a1873..6bb71e7c839 100644 --- a/src/LiveDevelopment/Agents/RemoteAgent.js +++ b/src/LiveDevelopment/Agents/RemoteAgent.js @@ -33,10 +33,11 @@ define(function RemoteAgent(require, exports, module) { "use strict"; - var LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), - EventDispatcher = require("utils/EventDispatcher"), - Inspector = require("LiveDevelopment/Inspector/Inspector"), - RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"); + var LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), + EventDispatcher = require("utils/EventDispatcher"), + Inspector = require("LiveDevelopment/Inspector/Inspector"), + RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"), + PreferencesManager = require("preferences/PreferencesManager"); var _load; // deferred load var _objectId; // the object id of the remote object @@ -130,7 +131,7 @@ define(function RemoteAgent(require, exports, module) { _stopKeepAliveInterval(); // inject RemoteFunctions - var command = "window._LD=" + RemoteFunctions + "(" + LiveDevelopment.config.experimental + ");"; + var command = "window._LD=" + RemoteFunctions + "(" + LiveDevelopment.config.experimental + "," + PreferencesManager.get("livedev.wsPort") + ");"; Inspector.Runtime.evaluate(command, function onEvaluate(response) { if (response.error || response.wasThrown) { diff --git a/src/LiveDevelopment/Agents/RemoteFunctions.js b/src/LiveDevelopment/Agents/RemoteFunctions.js index 5f302609f30..7060ce5a3fd 100644 --- a/src/LiveDevelopment/Agents/RemoteFunctions.js +++ b/src/LiveDevelopment/Agents/RemoteFunctions.js @@ -30,7 +30,7 @@ * modules should define a single function that returns an object of all * exported functions. */ -function RemoteFunctions(experimental) { +function RemoteFunctions(experimental, remoteWSPort) { "use strict"; var lastKeepAliveTime = Date.now(); @@ -98,6 +98,23 @@ function RemoteFunctions(experimental) { element.removeAttribute(key); } } + + // Checks if the element is in Viewport in the client browser + function isInViewport(element) { + var rect = element.getBoundingClientRect(); + var html = window.document.documentElement; + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || html.clientHeight) && + rect.right <= (window.innerWidth || html.clientWidth) + ); + } + + // returns the distance from the top of the closest relatively positioned parent element + function getDocumentOffsetTop(element) { + return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0); + } // construct the info menu function Menu(element) { @@ -319,6 +336,14 @@ function RemoteFunctions(experimental) { if (this.trigger) { _trigger(element, "highlight", 1); } + + if (!window.event && !isInViewport(element)) { + var top = getDocumentOffsetTop(element); + if (top) { + top -= (window.innerHeight / 2); + window.scrollTo(0, top); + } + } this.elements.push(element); this._makeHighlightDiv(element, doAnimation); @@ -824,7 +849,42 @@ function RemoteFunctions(experimental) { if (experimental) { window.document.addEventListener("keydown", onKeyDown); } - + + var _ws = null; + + function onDocumentClick(event) { + var element = event.target, + currentDataId, + newDataId; + + if (_ws && element && element.hasAttribute('data-brackets-id')) { + _ws.send(JSON.stringify({ + type: "message", + message: element.getAttribute('data-brackets-id') + })); + } + } + + + function createWebSocket() { + _ws = new WebSocket("ws://localhost:" + remoteWSPort); + _ws.onopen = function () { + window.document.addEventListener("click", onDocumentClick); + }; + + _ws.onmessage = function (evt) { + }; + + _ws.onclose = function () { + // websocket is closed + window.document.removeEventListener("click", onDocumentClick); + }; + } + + if (remoteWSPort) { + createWebSocket(); + } + return { "DOMEditHandler" : DOMEditHandler, "keepAlive" : keepAlive, diff --git a/src/LiveDevelopment/LiveDevelopment.js b/src/LiveDevelopment/LiveDevelopment.js index cd54b987b85..57d4438d118 100644 --- a/src/LiveDevelopment/LiveDevelopment.js +++ b/src/LiveDevelopment/LiveDevelopment.js @@ -94,7 +94,9 @@ define(function LiveDevelopment(require, exports, module) { ProjectManager = require("project/ProjectManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), - UserServer = require("LiveDevelopment/Servers/UserServer").UserServer; + UserServer = require("LiveDevelopment/Servers/UserServer").UserServer, + WebSocketTransport = require("LiveDevelopment/transports/WebSocketTransport"), + PreferencesManager = require("preferences/PreferencesManager"); // Inspector var Inspector = require("LiveDevelopment/Inspector/Inspector"); @@ -195,6 +197,10 @@ define(function LiveDevelopment(require, exports, module) { * Handles of registered servers */ var _regServers = []; + + PreferencesManager.definePreference("livedev.wsPort", "number", 8125, { + description: Strings.DESCRIPTION_LIVEDEV_WEBSOCKET_PORT + }); function _isPromisePending(promise) { return promise && promise.state() === "pending"; @@ -849,6 +855,7 @@ define(function LiveDevelopment(require, exports, module) { * @return {jQuery.Promise} Always return a resolved promise once the connection is closed */ function _close(doCloseWindow, reason) { + WebSocketTransport.closeWebSocketServer(); if (_closeDeferred) { return _closeDeferred; } else { @@ -1362,6 +1369,7 @@ define(function LiveDevelopment(require, exports, module) { // wait for server (StaticServer, Base URL or file:) prepareServerPromise .done(function () { + WebSocketTransport.createWebSocketServer(PreferencesManager.get("livedev.wsPort")); _doLaunchAfterServerReady(doc); }) .fail(function () { diff --git a/src/LiveDevelopment/transports/WebSocketTransport.js b/src/LiveDevelopment/transports/WebSocketTransport.js new file mode 100644 index 00000000000..9972bc85118 --- /dev/null +++ b/src/LiveDevelopment/transports/WebSocketTransport.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017 - present 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. + * + */ + +/** + * This transport provides a WebSocket connection between Brackets and a live browser preview. + * This is just a thin wrapper around the Node extension (WebSocketTransportDomain) 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"), + EditorManager = require("editor/EditorManager"), + HTMLInstrumentation = require("language/HTMLInstrumentation"); + + // The node extension that actually provides the WebSocket server. + + var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/WebSocketTransportDomain"; + + var WebSocketTransportDomain = new NodeDomain("webSocketTransport", domainPath); + + // Events + + WebSocketTransportDomain.on("message", function (obj, message) { + console.log("WebSocketTransport - event - message" + " - " + message); + var editor = EditorManager.getActiveEditor(), + position = HTMLInstrumentation.getPositionFromTagId(editor, parseInt(message, 10)); + if (position) { + editor.setCursorPos(position.line, position.ch, true); + } + }); + + function createWebSocketServer(port) { + WebSocketTransportDomain.exec("start", parseInt(port, 10)); + } + + function closeWebSocketServer() { + WebSocketTransportDomain.exec("close"); + } + + exports.createWebSocketServer = createWebSocketServer; + exports.closeWebSocketServer = closeWebSocketServer; +}); diff --git a/src/LiveDevelopment/transports/node/WebSocketTransportDomain.js b/src/LiveDevelopment/transports/node/WebSocketTransportDomain.js new file mode 100644 index 00000000000..2c6218c6c9e --- /dev/null +++ b/src/LiveDevelopment/transports/node/WebSocketTransportDomain.js @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2017 - present 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. + * + */ + +/** + * WebSocketTransportDomain creates a websocket server for Live Preview + * It receives the message containing tagID from the Remote Client(onClick) + * and emits an event which is listened by WebSocketTransport which + * brings the cursor to the tag corresponding to that particular tagID + */ + +/*eslint-env node */ +/*jslint node: true */ +"use strict"; + +var WebSocketServer = require("ws").Server; + +/** + * @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 + * Creates the WebSocketServer and handles incoming connections. + */ +function _createServer(socketPort) { + if (!_wsServer) { + // TODO: make port configurable, or use random port + _wsServer = new WebSocketServer({port: socketPort}); + _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("webSocketTransport: Error parsing message: " + msg); + return; + } + + if (msgObj.type === "message") { + _domainManager.emitEvent("webSocketTransport", "message", msgObj.message); + } else { + console.error("webSocketTransport: Got bad socket message type: " + msg); + } + }).on("error", function (e) { + console.error("webSocketTransport: Error on socket : " + e); + }).on("close", function () { + console.log("webSocketTransport closed"); + }); + }).on("error", function (e) { + console.error("webSocketTransport: Error on live preview server creation: " + e); + }); + } +} + +/** + * Initializes the socket server. + * @param {number} port + */ +function _cmdStart(port) { + _createServer(port); +} + +/** + * Kill the WebSocketServer + */ +function _cmdClose() { + if (_wsServer) { + _wsServer.close(); + _wsServer = null; + } +} + +/** + * Initializes the domain and registers commands. + * @param {DomainManager} domainManager The DomainManager for the server + */ +function init(domainManager) { + _domainManager = domainManager; + if (!domainManager.hasDomain("webSocketTransport")) { + domainManager.registerDomain("webSocketTransport", {major: 0, minor: 1}); + } + + domainManager.registerEvent( + "webSocketTransport", + "message", + [ + { + name: "msg", + type: "string", + description: "JSON message from client page" + } + ] + ); + + domainManager.registerCommand( + "webSocketTransport", // domain name + "start", // command name + _cmdStart, // command handler function + false, // this command is synchronous in Node + "Creates the WS server", + [ + { + name: "port", + type: "number", + description: "Port on which server needs to listen" + } + ], + [] + ); + + domainManager.registerCommand( + "webSocketTransport", // domain name + "close", // command name + _cmdClose, // command handler function + false, // this command is synchronous in Node + "Kills the websocket server", + [] + ); +} + +exports.init = init; diff --git a/src/config.json b/src/config.json index 78c75f94a01..619a19b0a3b 100644 --- a/src/config.json +++ b/src/config.json @@ -38,7 +38,8 @@ "dependencies": { "anymatch": "1.3.0", "chokidar": "1.6.0", - "lodash": "4.15.0" + "lodash": "4.15.0", + "ws": "~0.4.31" }, "devDependencies": { "glob": "7.0.6", diff --git a/src/language/HTMLInstrumentation.js b/src/language/HTMLInstrumentation.js index f763ec83e63..75efc3fc660 100644 --- a/src/language/HTMLInstrumentation.js +++ b/src/language/HTMLInstrumentation.js @@ -53,7 +53,8 @@ define(function (require, exports, module) { var DocumentManager = require("document/DocumentManager"), HTMLSimpleDOM = require("./HTMLSimpleDOM"), - HTMLDOMDiff = require("./HTMLDOMDiff"); + HTMLDOMDiff = require("./HTMLDOMDiff"), + _ = require("thirdparty/lodash"); var allowIncremental = true; @@ -87,6 +88,21 @@ define(function (require, exports, module) { return pos1 && pos2 && pos1.line === pos2.line && pos1.ch === pos2.ch; } + function getPositionFromTagId(editor, tagId) { + var marks = editor._codeMirror.getAllMarks(), + i, + markFound; + + markFound = _.find(marks, function (mark) { + return (mark.tagID === tagId); + }); + if (markFound) { + return markFound.find().from; + } else { + return null; + } + } + /** * @private * Filters the given marks to find the ones that correspond to instrumented tags, @@ -805,6 +821,7 @@ define(function (require, exports, module) { exports._markText = _markText; exports._getMarkerAtDocumentPos = _getMarkerAtDocumentPos; exports._getTagIDAtDocumentPos = _getTagIDAtDocumentPos; + exports.getPositionFromTagId = getPositionFromTagId; exports._markTextFromDOM = _markTextFromDOM; exports._updateDOM = _updateDOM; exports._allowIncremental = allowIncremental; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 193cba4193e..3541b304ac1 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -789,5 +789,6 @@ define({ "DEFAULT_PREFERENCES_JSON_DEFAULT" : "Default", "DESCRIPTION_PURE_CODING_SURFACE" : "true to enable code only mode and hide all other UI elements in {APP_NAME}", "DESCRIPTION_INDENT_LINE_COMMENT" : "true to enable indenting of line comments", - "DESCRIPTION_RECENT_FILES_NAV" : "Enable/disable navigation in recent files" + "DESCRIPTION_RECENT_FILES_NAV" : "Enable/disable navigation in recent files", + "DESCRIPTION_LIVEDEV_WEBSOCKET_PORT" : "Port on which WebSocket Server runs for Live Preview" });