diff --git a/.eslintrc.js b/.eslintrc.js index 40331166ab1..4ff0306efcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -80,5 +80,15 @@ module.exports = { "Uint32Array": false, "WebSocket": false, "XMLHttpRequest": false + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "script", + "ecmaFeatures": { + "arrowFunctions": true, + "binaryLiterals": true, + "blockBindings": true, + "classes": true + } } }; diff --git a/Gruntfile.js b/Gruntfile.js index 78c3b4f6986..b6598f4e3af 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -50,6 +50,16 @@ module.exports = function (grunt) { 'src/styles/brackets.css' ] }] + }, + node_modules_test_dir : { + files: [{ + dot: true, + src: [ + 'dist/node_modules/npm/test/fixtures', + 'dist/node_modules/npm/node_modules/tar/test', + 'dist/node_modules/npm/node_modules/npm-registry-client/test' + ] + }] } }, copy: { @@ -82,6 +92,9 @@ module.exports = function (grunt) { src: [ 'extensibility/node/**', 'JSUtils/node/**', + 'languageTools/node/**', + 'languageTools/styles/**', + 'languageTools/LanguageClient/**', '!extensibility/node/spec/**', '!extensibility/node/node_modules/**/{test,tst}/**/*', '!extensibility/node/node_modules/**/examples/**/*', @@ -393,9 +406,7 @@ module.exports = function (grunt) { // Update version number in package.json and rewrite src/config.json grunt.registerTask('set-release', ['update-release-number', 'write-config:dev']); - // task: build - grunt.registerTask('build', [ - 'write-config:dist', + grunt.registerTask('build-common', [ 'eslint:src', 'jasmine', 'clean', @@ -411,9 +422,23 @@ module.exports = function (grunt) { 'npm-install', 'cleanempty', 'usemin', - 'build-config' + 'build-config', + 'clean:node_modules_test_dir' + ]); + + // task: build + grunt.registerTask('build', [ + 'write-config:dist', + 'build-common' + ]); + + // task: build + grunt.registerTask('build-prerelease', [ + 'write-config:prerelease', + 'build-common' ]); // Default task. grunt.registerTask('default', ['test']); }; + diff --git a/README.md b/README.md index 481d454ef3f..a71783965e9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ + +| :warning: On September 1, 2021, Adobe will end support for Brackets. If you would like to continue using, maintaining, and improving Brackets, you may fork the project on [GitHub](https://github.com/adobe/brackets). Through Adobe’s partnership with Microsoft, we encourage users to migrate to [Visual Studio Code](https://aka.ms/brackets-to-vscode), Microsoft’s free code editor built on open source. +| --- + + Welcome to Brackets! [![Build Status](https://travis-ci.org/adobe/brackets.svg?branch=master)](https://travis-ci.org/adobe/brackets) ------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3ac8cb72839..acfb3ca93ec 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1946,9 +1946,9 @@ "dev": true }, "lodash": { - "version": "4.17.4", - "from": "lodash@4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" + "version": "4.17.15", + "from": "lodash@4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz" }, "longest": { "version": "1.0.1", diff --git a/package.json b/package.json index 67a939326f1..84ce74f281c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Brackets", - "version": "1.13.0-0", - "apiVersion": "1.13.0", + "version": "1.15.0-0", + "apiVersion": "1.15.0", "homepage": "http://brackets.io", "issues": { "url": "http://github.com/adobe/brackets/issues" @@ -21,7 +21,7 @@ "chokidar": "1.6.1", "decompress-zip": "0.3.0", "fs-extra": "2.0.0", - "lodash": "4.17.4", + "lodash": "4.17.15", "npm": "3.10.10", "opn": "4.0.2", "request": "2.79.0", @@ -64,7 +64,7 @@ "scripts": { "prepush": "npm run eslint", "postinstall": "grunt install", - "test": "grunt test cla-check-pull", + "test": "grunt test", "eslint": "grunt eslint" }, "licenses": [ diff --git a/src/LiveDevelopment/LiveDevelopment.js b/src/LiveDevelopment/LiveDevelopment.js index 93cd81f16f9..22e677ae372 100644 --- a/src/LiveDevelopment/LiveDevelopment.js +++ b/src/LiveDevelopment/LiveDevelopment.js @@ -96,7 +96,8 @@ define(function LiveDevelopment(require, exports, module) { StringUtils = require("utils/StringUtils"), UserServer = require("LiveDevelopment/Servers/UserServer").UserServer, WebSocketTransport = require("LiveDevelopment/transports/WebSocketTransport"), - PreferencesManager = require("preferences/PreferencesManager"); + PreferencesManager = require("preferences/PreferencesManager"), + HealthLogger = require("utils/HealthLogger"); // Inspector var Inspector = require("LiveDevelopment/Inspector/Inspector"); @@ -1350,6 +1351,13 @@ define(function LiveDevelopment(require, exports, module) { }); } } + // Send analytics data when Live Preview is opened + HealthLogger.sendAnalyticsData( + "livePreviewOpen", + "usage", + "livePreview", + "open" + ); // Register user defined server provider and keep handlers for further clean-up _regServers.push(LiveDevServerManager.registerServer({ create: _createUserServer }, 99)); diff --git a/src/base-config/keyboard.json b/src/base-config/keyboard.json index aa9c67cd4d5..5d7ea339515 100644 --- a/src/base-config/keyboard.json +++ b/src/base-config/keyboard.json @@ -162,6 +162,9 @@ "cmd.findInFiles": [ "Ctrl-Shift-F" ], + "cmd.findAllReferences": [ + "Shift-F12" + ], "cmd.replaceInFiles": [ { "key": "Ctrl-Shift-H" @@ -281,6 +284,9 @@ "navigate.gotoDefinition": [ "Ctrl-T" ], + "navigate.gotoDefinitionInProject": [ + "Ctrl-Shift-T" + ], "navigate.jumptoDefinition": [ "Ctrl-J" ], diff --git a/src/brackets.config.dev.json b/src/brackets.config.dev.json index 8f567b28d21..76137056939 100644 --- a/src/brackets.config.dev.json +++ b/src/brackets.config.dev.json @@ -2,5 +2,8 @@ "healthDataServerURL" : "https://healthdev.brackets.io/healthDataLog", "analyticsDataServerURL" : "https://cc-api-data-stage.adobe.io/ingest", "serviceKey" : "brackets-service", - "environment" : "stage" + "environment" : "stage", + "update_info_url" : "https://s3.amazonaws.com/files.brackets.io/updates/prerelease/.json", + "notification_info_url" : "https://s3.amazonaws.com/files.brackets.io/notifications/prerelease/.json", + "buildtype" : "dev" } diff --git a/src/brackets.config.dist.json b/src/brackets.config.dist.json index e84b7671072..e6168390ade 100644 --- a/src/brackets.config.dist.json +++ b/src/brackets.config.dist.json @@ -2,5 +2,8 @@ "healthDataServerURL" : "https://health.brackets.io/healthDataLog", "analyticsDataServerURL" : "https://cc-api-data.adobe.io/ingest", "serviceKey" : "brackets-service", - "environment" : "production" + "environment" : "production", + "update_info_url" : "https://getupdates.brackets.io/getupdates?locale=", + "notification_info_url" : "https://getupdates.brackets.io/getnotifications?locale=", + "buildtype" : "production" } diff --git a/src/brackets.config.json b/src/brackets.config.json index 89a0db49067..12f8d25b1aa 100644 --- a/src/brackets.config.json +++ b/src/brackets.config.json @@ -4,7 +4,6 @@ "app_title" : "Brackets", "app_name_about" : "Brackets", "about_icon" : "styles/images/brackets_icon.svg", - "update_info_url" : "https://getupdates.brackets.io/getupdates/", "how_to_use_url" : "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "support_url" : "https://github.com/adobe/brackets/wiki/Troubleshooting", "suggest_feature_url" : "https://github.com/adobe/brackets/wiki/Suggest-a-Feature", diff --git a/src/brackets.config.prerelease.json b/src/brackets.config.prerelease.json new file mode 100644 index 00000000000..ac8e9de01e9 --- /dev/null +++ b/src/brackets.config.prerelease.json @@ -0,0 +1,9 @@ +{ + "healthDataServerURL" : "https://health.brackets.io/healthDataLog", + "analyticsDataServerURL" : "https://cc-api-data.adobe.io/ingest", + "serviceKey" : "brackets-service", + "environment" : "production", + "update_info_url" : "https://s3.amazonaws.com/files.brackets.io/updates/prerelease/.json", + "notification_info_url" : "https://s3.amazonaws.com/files.brackets.io/notifications/prerelease/.json", + "buildtype" : "prerelease" +} diff --git a/src/brackets.js b/src/brackets.js index 9a21072cefd..c291a32216e 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -137,6 +137,10 @@ define(function (require, exports, module) { return PathUtils; } }); + + //load language features + require("features/ParameterHintsManager"); + require("features/JumpToDefManager"); // Load modules that self-register and just need to get included in the main project require("command/DefaultMenus"); @@ -151,10 +155,22 @@ define(function (require, exports, module) { require("search/FindInFilesUI"); require("search/FindReplace"); + //Load find References Feature Manager + require("features/FindReferencesManager"); + //Load common JS module require("JSUtils/Session"); require("JSUtils/ScopeManager"); + //load Language Tools Module + require("languageTools/PathConverters"); + require("languageTools/LanguageTools"); + require("languageTools/ClientLoader"); + require("languageTools/BracketsToNodeInterface"); + require("languageTools/DefaultProviders"); + require("languageTools/DefaultEventHandlers"); + + PerfUtils.addMeasurement("brackets module dependencies resolved"); // Local variables @@ -241,6 +257,23 @@ define(function (require, exports, module) { ); } + brackets.app.getRemoteDebuggingPort(function (err, remote_debugging_port){ + var InfoBar = require('widgets/infobar'), + StringUtils = require("utils/StringUtils"); + if ((!err) && remote_debugging_port && remote_debugging_port > 0) { + InfoBar.showInfoBar({ + type: "warning", + title: `${Strings.REMOTE_DEBUGGING_ENABLED}${remote_debugging_port}`, + description: "" + }); + } else if (err) { + InfoBar.showInfoBar({ + type: "error", + title: StringUtils.format(Strings.REMOTE_DEBUGGING_PORT_INVALID, err, 1024, 65534), + description: "" + }); + } + }); // Use quiet scrollbars if we aren't on Lion. If we're on Lion, only // use native scroll bars when the mouse is not plugged in or when // using the "Always" scroll bar setting. diff --git a/src/command/Commands.js b/src/command/Commands.js index e607880457e..d0f0087676b 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -97,6 +97,7 @@ define(function (require, exports, module) { exports.CMD_REPLACE = "cmd.replace"; // FindReplace.js _replace() exports.CMD_REPLACE_IN_FILES = "cmd.replaceInFiles"; // FindInFilesUI.js _showReplaceBar() exports.CMD_REPLACE_IN_SUBTREE = "cmd.replaceInSubtree"; // FindInFilesUI.js _showReplaceBarForSubtree() + exports.CMD_FIND_ALL_REFERENCES = "cmd.findAllReferences"; // findReferencesManager.js _openReferencesPanel() // VIEW exports.CMD_THEMES_OPEN_SETTINGS = "view.themesOpenSetting"; // MenuCommands.js Settings.open() @@ -123,8 +124,9 @@ define(function (require, exports, module) { exports.NAVIGATE_SHOW_IN_FILE_TREE = "navigate.showInFileTree"; // DocumentCommandHandlers.js handleShowInTree() exports.NAVIGATE_SHOW_IN_OS = "navigate.showInOS"; // DocumentCommandHandlers.js handleShowInOS() exports.NAVIGATE_QUICK_OPEN = "navigate.quickOpen"; // QuickOpen.js doFileSearch() - exports.NAVIGATE_JUMPTO_DEFINITION = "navigate.jumptoDefinition"; // EditorManager.js _doJumpToDef() + exports.NAVIGATE_JUMPTO_DEFINITION = "navigate.jumptoDefinition"; // JumpToDefManager.js _doJumpToDef() exports.NAVIGATE_GOTO_DEFINITION = "navigate.gotoDefinition"; // QuickOpen.js doDefinitionSearch() + exports.NAVIGATE_GOTO_DEFINITION_PROJECT = "navigate.gotoDefinitionInProject"; // QuickOpen.js doDefinitionSearchInProject() exports.NAVIGATE_GOTO_LINE = "navigate.gotoLine"; // QuickOpen.js doGotoLine() exports.NAVIGATE_GOTO_FIRST_PROBLEM = "navigate.gotoFirstProblem"; // CodeInspection.js handleGotoFirstProblem() exports.TOGGLE_QUICK_EDIT = "navigate.toggleQuickEdit"; // EditorManager.js _toggleInlineWidget() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 2eb6f871925..86e6776d1fd 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -136,6 +136,7 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.CMD_SKIP_CURRENT_MATCH); menu.addMenuDivider(); menu.addMenuItem(Commands.CMD_FIND_IN_FILES); + menu.addMenuItem(Commands.CMD_FIND_ALL_REFERENCES); menu.addMenuDivider(); menu.addMenuItem(Commands.CMD_REPLACE); menu.addMenuItem(Commands.CMD_REPLACE_IN_FILES); @@ -172,6 +173,7 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.NAVIGATE_QUICK_OPEN); menu.addMenuItem(Commands.NAVIGATE_GOTO_LINE); menu.addMenuItem(Commands.NAVIGATE_GOTO_DEFINITION); + menu.addMenuItem(Commands.NAVIGATE_GOTO_DEFINITION_PROJECT); menu.addMenuItem(Commands.NAVIGATE_JUMPTO_DEFINITION); menu.addMenuItem(Commands.NAVIGATE_GOTO_FIRST_PROBLEM); menu.addMenuDivider(); @@ -282,6 +284,7 @@ define(function (require, exports, module) { // editor_cmenu.addMenuItem(Commands.NAVIGATE_JUMPTO_DEFINITION); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_DOCS); + editor_cmenu.addMenuItem(Commands.CMD_FIND_ALL_REFERENCES); editor_cmenu.addMenuDivider(); editor_cmenu.addMenuItem(Commands.EDIT_CUT); editor_cmenu.addMenuItem(Commands.EDIT_COPY); diff --git a/src/config.json b/src/config.json index e5eecc04596..d954955a7c2 100644 --- a/src/config.json +++ b/src/config.json @@ -3,7 +3,6 @@ "app_title": "Brackets", "app_name_about": "Brackets", "about_icon": "styles/images/brackets_icon.svg", - "update_info_url": "https://getupdates.brackets.io/getupdates/", "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "support_url": "https://github.com/adobe/brackets/wiki/Troubleshooting", "suggest_feature_url": "https://github.com/adobe/brackets/wiki/Suggest-a-Feature", @@ -23,11 +22,12 @@ "healthDataServerURL": "https://healthdev.brackets.io/healthDataLog", "analyticsDataServerURL": "https://cc-api-data-stage.adobe.io/ingest", "serviceKey": "brackets-service", - "environment": "stage" + "environment": "stage", + "update_info_url": "https://s3.amazonaws.com/files.brackets.io/updates/prerelease/.json" }, "name": "Brackets", - "version": "1.13.0-0", - "apiVersion": "1.13.0", + "version": "1.15.0-0", + "apiVersion": "1.15.0", "homepage": "http://brackets.io", "issues": { "url": "http://github.com/adobe/brackets/issues" @@ -47,7 +47,7 @@ "chokidar": "1.6.1", "decompress-zip": "0.3.0", "fs-extra": "2.0.0", - "lodash": "4.17.4", + "lodash": "4.17.15", "npm": "3.10.10", "opn": "4.0.2", "request": "2.79.0", @@ -90,7 +90,7 @@ "scripts": { "prepush": "npm run eslint", "postinstall": "grunt install", - "test": "grunt test cla-check-pull", + "test": "grunt test", "eslint": "grunt eslint" }, "licenses": [ @@ -99,4 +99,4 @@ "url": "https://github.com/adobe/brackets/blob/master/LICENSE" } ] -} +} \ No newline at end of file diff --git a/src/document/Document.js b/src/document/Document.js index fce97e4cf1a..a013008bb0e 100644 --- a/src/document/Document.js +++ b/src/document/Document.js @@ -73,6 +73,7 @@ define(function (require, exports, module) { */ function Document(file, initialTimestamp, rawText) { this.file = file; + this.editable = !file.readOnly; this._updateLanguage(); this.refreshText(rawText, initialTimestamp, true); // List of full editors which are initialized as master editors for this doc. diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index a895ec16fb2..bcc235f9a62 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -31,6 +31,7 @@ define(function (require, exports, module) { CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), DeprecationWarning = require("utils/DeprecationWarning"), + EventDispatcher = require("utils/EventDispatcher"), ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), MainViewManager = require("view/MainViewManager"), @@ -135,7 +136,14 @@ define(function (require, exports, module) { PreferencesManager.definePreference("defaultExtension", "string", "", { excludeFromHints: true }); + EventDispatcher.makeEventDispatcher(exports); + /** + * Event triggered when File Save is cancelled, when prompted to save dirty files + */ + var APP_QUIT_CANCELLED = "appQuitCancelled"; + + /** * JSLint workaround for circular dependency * @type {function} @@ -149,7 +157,9 @@ define(function (require, exports, module) { function _updateTitle() { var currentDoc = DocumentManager.getCurrentDocument(), windowTitle = brackets.config.app_title, - currentlyViewedPath = MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE); + currentlyViewedFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE), + currentlyViewedPath = currentlyViewedFile && currentlyViewedFile.fullPath, + readOnlyString = (currentlyViewedFile && currentlyViewedFile.readOnly) ? "[Read Only] - " : ""; if (!brackets.nativeMenus) { if (currentlyViewedPath) { @@ -189,7 +199,7 @@ define(function (require, exports, module) { var projectName = projectRoot.name; // Construct shell/browser window title, e.g. "• index.html (myProject) — Brackets" if (currentlyViewedPath) { - windowTitle = StringUtils.format(WINDOW_TITLE_STRING_DOC, _currentTitlePath, projectName, brackets.config.app_title); + windowTitle = StringUtils.format(WINDOW_TITLE_STRING_DOC, readOnlyString + _currentTitlePath, projectName, brackets.config.app_title); // Display dirty dot when there are unsaved changes if (currentDoc && currentDoc.isDirty) { windowTitle = "• " + windowTitle; @@ -679,6 +689,15 @@ define(function (require, exports, module) { var doc = DocumentManager.createUntitledDocument(_nextUntitledIndexToUse++, defaultExtension); MainViewManager._edit(MainViewManager.ACTIVE_PANE, doc); + HealthLogger.sendAnalyticsData( + HealthLogger.commonStrings.USAGE + + HealthLogger.commonStrings.FILE_OPEN + + HealthLogger.commonStrings.FILE_NEW, + HealthLogger.commonStrings.USAGE, + HealthLogger.commonStrings.FILE_OPEN, + HealthLogger.commonStrings.FILE_NEW + ); + return new $.Deferred().resolve(doc).promise(); } @@ -778,6 +797,7 @@ define(function (require, exports, module) { .done(function () { docToSave.notifySaved(); result.resolve(file); + HealthLogger.fileSaved(docToSave); }) .fail(function (err) { if (err === FileSystemError.CONTENTS_MODIFIED) { @@ -847,6 +867,14 @@ define(function (require, exports, module) { return result.promise(); } + + /** + * Dispatches the app quit cancelled event + */ + function dispatchAppQuitCancelledEvent() { + exports.trigger(exports.APP_QUIT_CANCELLED); + } + /** * Opens the native OS save as dialog and saves document. @@ -949,6 +977,7 @@ define(function (require, exports, module) { } else { openNewFile(); } + HealthLogger.fileSaved(doc); }) .fail(function (error) { _showSaveFileError(error, path) @@ -996,6 +1025,7 @@ define(function (require, exports, module) { if (selectedPath) { _doSaveAfterSaveDialog(selectedPath); } else { + dispatchAppQuitCancelledEvent(); result.reject(USER_CANCELED); } } else { @@ -1167,6 +1197,7 @@ define(function (require, exports, module) { function doClose(file) { if (!promptOnly) { MainViewManager._close(paneId, file); + HealthLogger.fileClosed(file); } } @@ -1217,6 +1248,7 @@ define(function (require, exports, module) { ) .done(function (id) { if (id === Dialogs.DIALOG_BTN_CANCEL) { + dispatchAppQuitCancelledEvent(); result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // "Save" case: wait until we confirm save has succeeded before closing @@ -1322,6 +1354,7 @@ define(function (require, exports, module) { ) .done(function (id) { if (id === Dialogs.DIALOG_BTN_CANCEL) { + dispatchAppQuitCancelledEvent(); result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // Save all unsaved files, then if that succeeds, close all @@ -1609,28 +1642,33 @@ define(function (require, exports, module) { if (brackets.inBrowser) { result.resolve(); } else { - var port = brackets.app.getRemoteDebuggingPort ? brackets.app.getRemoteDebuggingPort() : 9234; - Inspector.getDebuggableWindows("127.0.0.1", port) - .fail(result.reject) - .done(function (response) { - var page = response[0]; - if (!page || !page.webSocketDebuggerUrl) { - result.reject(); - return; - } - var _socket = new WebSocket(page.webSocketDebuggerUrl); - // Disable the cache - _socket.onopen = function _onConnect() { - _socket.send(JSON.stringify({ id: 1, method: "Network.setCacheDisabled", params: { "cacheDisabled": true } })); - }; - // The first message will be the confirmation => disconnected to allow remote debugging of Brackets - _socket.onmessage = function _onMessage(e) { - _socket.close(); - result.resolve(); - }; - // In case of an error - _socket.onerror = result.reject; - }); + brackets.app.getRemoteDebuggingPort(function (err, port){ + if ((!err) && port && port > 0) { + Inspector.getDebuggableWindows("127.0.0.1", port) + .fail(result.reject) + .done(function (response) { + var page = response[0]; + if (!page || !page.webSocketDebuggerUrl) { + result.reject(); + return; + } + var _socket = new WebSocket(page.webSocketDebuggerUrl); + // Disable the cache + _socket.onopen = function _onConnect() { + _socket.send(JSON.stringify({ id: 1, method: "Network.setCacheDisabled", params: { "cacheDisabled": true } })); + }; + // The first message will be the confirmation => disconnected to allow remote debugging of Brackets + _socket.onmessage = function _onMessage(e) { + _socket.close(); + result.resolve(); + }; + // In case of an error + _socket.onerror = result.reject; + }); + } else { + result.reject(); + } + }); } return result.promise(); @@ -1722,28 +1760,29 @@ define(function (require, exports, module) { /** * Attach a beforeunload handler to notify user about unsaved changes and URL redirection in CEF. * Prevents data loss in scenario reported under #13708 + * Make sure we don't attach this handler if the current window is actually a test window **/ - window.onbeforeunload = function(e) { - if (window.location.pathname.indexOf('SpecRunner') > -1) { - return; - } - var openDocs = DocumentManager.getAllOpenDocuments(); + var isTestWindow = (new window.URLSearchParams(window.location.search || "")).get("testEnvironment"); + if (!isTestWindow) { + window.onbeforeunload = function(e) { + var openDocs = DocumentManager.getAllOpenDocuments(); - // Detect any unsaved changes - openDocs = openDocs.filter(function(doc) { - return doc && doc.isDirty; - }); + // Detect any unsaved changes + openDocs = openDocs.filter(function(doc) { + return doc && doc.isDirty; + }); - // Ensure we are not in normal app-quit or reload workflow - if (!_isReloading && !_windowGoingAway) { - if (openDocs.length > 0) { - return Strings.WINDOW_UNLOAD_WARNING_WITH_UNSAVED_CHANGES; - } else { - return Strings.WINDOW_UNLOAD_WARNING; + // Ensure we are not in normal app-quit or reload workflow + if (!_isReloading && !_windowGoingAway) { + if (openDocs.length > 0) { + return Strings.WINDOW_UNLOAD_WARNING_WITH_UNSAVED_CHANGES; + } else { + return Strings.WINDOW_UNLOAD_WARNING; + } } - } - }; + }; + } /** Do some initialization when the DOM is ready **/ AppInit.htmlReady(function () { @@ -1783,6 +1822,8 @@ define(function (require, exports, module) { // Define public API exports.showFileOpenError = showFileOpenError; + exports.APP_QUIT_CANCELLED = APP_QUIT_CANCELLED; + // Deprecated commands CommandManager.register(Strings.CMD_ADD_TO_WORKING_SET, Commands.FILE_ADD_TO_WORKING_SET, handleFileAddToWorkingSet); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 5841bc03372..4cebfcfab88 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -323,10 +323,11 @@ define(function (require, exports, module) { * If all you need is the Document's getText() value, use the faster getDocumentText() instead. * * @param {!string} fullPath + * @param {!object} fileObj actual File|RemoteFile or some other protocol adapter handle * @return {$.Promise} A promise object that will be resolved with the Document, or rejected * with a FileSystemError if the file is not yet open and can't be read from disk. */ - function getDocumentForPath(fullPath) { + function getDocumentForPath(fullPath, fileObj) { var doc = getOpenDocumentForPath(fullPath); if (doc) { @@ -342,7 +343,7 @@ define(function (require, exports, module) { return promise; } - var file = FileSystem.getFileForPath(fullPath), + var file = fileObj || FileSystem.getFileForPath(fullPath), pendingPromise = getDocumentForPath._pendingDocumentPromises[file.id]; if (pendingPromise) { diff --git a/src/editor/CSSInlineEditor.js b/src/editor/CSSInlineEditor.js index 363f722ed9c..e51a280f4d9 100644 --- a/src/editor/CSSInlineEditor.js +++ b/src/editor/CSSInlineEditor.js @@ -39,6 +39,7 @@ define(function (require, exports, module) { MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor"), Strings = require("strings"), ViewUtils = require("utils/ViewUtils"), + HealthLogger = require("utils/HealthLogger"), _ = require("thirdparty/lodash"); var _newRuleCmd, @@ -169,6 +170,14 @@ define(function (require, exports, module) { return null; } + //Send analytics data for QuickEdit open + HealthLogger.sendAnalyticsData( + "QuickEditOpen", + "usage", + "quickEdit", + "open" + ); + // Only provide CSS editor if the selection is within a single line var sel = hostEditor.getSelection(); if (sel.start.line !== sel.end.line) { diff --git a/src/editor/CodeHintList.js b/src/editor/CodeHintList.js index 964e1ed447c..23380301d2d 100644 --- a/src/editor/CodeHintList.js +++ b/src/editor/CodeHintList.js @@ -152,7 +152,7 @@ define(function (require, exports, module) { ViewUtils.scrollElementIntoView($view, $item, false); if (this.handleHighlight) { - this.handleHighlight($item.find("a")); + this.handleHighlight($item.find("a"), this.$hintMenu.find("#codehint-desc")); } } }; @@ -191,6 +191,7 @@ define(function (require, exports, module) { this.hints = hintObj.hints; this.hints.handleWideResults = hintObj.handleWideResults; + this.enableDescription = hintObj.enableDescription; // if there is no match, assume name is already a formatted jQuery // object; otherwise, use match to format name for display. @@ -265,6 +266,13 @@ define(function (require, exports, module) { // attach to DOM $parent.append($ul); + // If a a description field requested attach one + if (this.enableDescription) { + // Remove the desc element first to ensure DOM order + $parent.find("#codehint-desc").remove(); + $parent.append(""); + $ul.addClass("withDesc"); + } this._setSelectedIndex(selectInitial ? 0 : -1); } }; @@ -283,7 +291,9 @@ define(function (require, exports, module) { textHeight = this.editor.getTextHeight(), $window = $(window), $menuWindow = this.$hintMenu.children("ul"), - menuHeight = $menuWindow.outerHeight(); + $descElement = this.$hintMenu.find("#codehint-desc"), + descOverhang = $descElement.length === 1 ? $descElement.height() : 0, + menuHeight = $menuWindow.outerHeight() + descOverhang; // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code // adjust positioning so menu is not clipped off bottom or right @@ -304,6 +314,13 @@ define(function (require, exports, module) { availableWidth = menuWidth + Math.abs(rightOverhang); } + //Creating the offset element for hint description element + var descOffset = this.$hintMenu.find("ul.dropdown-menu")[0].getBoundingClientRect().height; + if (descOffset === 0) { + descOffset = menuHeight - descOverhang; + } + this.$hintMenu.find("#codehint-desc").css("margin-top", descOffset - 1); + return {left: posLeft, top: posTop, width: availableWidth}; }; diff --git a/src/editor/CodeHintManager.js b/src/editor/CodeHintManager.js index 8a2cdf28e69..925ffcb61ec 100644 --- a/src/editor/CodeHintManager.js +++ b/src/editor/CodeHintManager.js @@ -507,10 +507,21 @@ define(function (require, exports, module) { sessionEditor = editor; hintList = new CodeHintList(sessionEditor, insertHintOnTab, maxCodeHints); - hintList.onHighlight(function ($hint) { - // If the current hint provider listening for hint item highlight change - if (sessionProvider.onHighlight) { - sessionProvider.onHighlight($hint); + hintList.onHighlight(function ($hint, $hintDescContainer) { + if (hintList.enableDescription && $hintDescContainer && $hintDescContainer.length) { + // If the current hint provider listening for hint item highlight change + if (sessionProvider.onHighlight) { + sessionProvider.onHighlight($hint, $hintDescContainer); + } + + // Update the hint description + if (sessionProvider.updateHintDescription) { + sessionProvider.updateHintDescription($hint, $hintDescContainer); + } + } else { + if (sessionProvider.onHighlight) { + sessionProvider.onHighlight($hint); + } } }); hintList.onSelect(function (hint) { diff --git a/src/editor/Editor.js b/src/editor/Editor.js index 03e7916d283..3a009056ad4 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -323,7 +323,7 @@ define(function (require, exports, module) { function Editor(document, makeMasterEditor, container, range, options) { var self = this; - var isReadOnly = options && options.isReadOnly; + var isReadOnly = (options && options.isReadOnly) || !document.editable; _instances.push(this); diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index 79897d47118..e9df922d40b 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -92,15 +92,6 @@ define(function (require, exports, module) { */ var _inlineDocsProviders = []; - /** - * Registered jump-to-definition providers. - * @see {@link #registerJumpToDefProvider}. - * @private - * @type {Array.} - */ - var _jumpToDefProviders = []; - - /** * DOM element to house any hidden editors created soley for inline widgets * @private @@ -423,19 +414,6 @@ define(function (require, exports, module) { _insertProviderSorted(_inlineDocsProviders, provider, priority); } - /** - * Registers a new jump-to-definition provider. When jump-to-definition is invoked each - * registered provider is asked if it wants to provide jump-to-definition results, given - * the current editor and cursor location. - * - * @param {function(!Editor, !{line:number, ch:number}):?$.Promise} provider - * The provider returns a promise that is resolved whenever it's done handling the operation, - * or returns null to indicate the provider doesn't want to respond to this case. It is entirely - * up to the provider to open the file containing the definition, select the appropriate text, etc. - */ - function registerJumpToDefProvider(provider) { - _jumpToDefProviders.push(provider); - } /** * @private @@ -705,55 +683,6 @@ define(function (require, exports, module) { return _lastFocusedEditor; } - - /** - * Asynchronously asks providers to handle jump-to-definition. - * @return {!Promise} Resolved when the provider signals that it's done; rejected if no - * provider responded or the provider that responded failed. - */ - function _doJumpToDef() { - var providers = _jumpToDefProviders; - var promise, - i, - result = new $.Deferred(); - - var editor = getActiveEditor(); - - if (editor) { - var pos = editor.getCursorPos(); - - PerfUtils.markStart(PerfUtils.JUMP_TO_DEFINITION); - - // Run through providers until one responds - for (i = 0; i < providers.length && !promise; i++) { - var provider = providers[i]; - promise = provider(editor, pos); - } - - // Will one of them will provide a result? - if (promise) { - promise.done(function () { - PerfUtils.addMeasurement(PerfUtils.JUMP_TO_DEFINITION); - result.resolve(); - }).fail(function () { - // terminate timer that was started above - PerfUtils.finalizeMeasurement(PerfUtils.JUMP_TO_DEFINITION); - result.reject(); - }); - } else { - // terminate timer that was started above - PerfUtils.finalizeMeasurement(PerfUtils.JUMP_TO_DEFINITION); - result.reject(); - } - - } else { - result.reject(); - } - - return result.promise(); - } - - /** * file removed from pane handler. * @param {jQuery.Event} e @@ -797,10 +726,6 @@ define(function (require, exports, module) { CommandManager.register(Strings.CMD_TOGGLE_QUICK_DOCS, Commands.TOGGLE_QUICK_DOCS, function () { return _toggleInlineWidget(_inlineDocsProviders, Strings.ERROR_QUICK_DOCS_PROVIDER_NOT_FOUND); }); - CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, Commands.NAVIGATE_JUMPTO_DEFINITION, _doJumpToDef); - - // Create PerfUtils measurement - PerfUtils.createPerfMeasurement("JUMP_TO_DEFINITION", "Jump-To-Definiiton"); MainViewManager.on("currentFileChange", _handleCurrentFileChange); MainViewManager.on("workingSetRemove workingSetRemoveList", _handleRemoveFromPaneView); @@ -830,7 +755,6 @@ define(function (require, exports, module) { exports.registerInlineEditProvider = registerInlineEditProvider; exports.registerInlineDocsProvider = registerInlineDocsProvider; - exports.registerJumpToDefProvider = registerJumpToDefProvider; // Deprecated exports.registerCustomViewer = registerCustomViewer; diff --git a/src/editor/EditorStatusBar.js b/src/editor/EditorStatusBar.js index d7ebbcead66..7d27e926424 100644 --- a/src/editor/EditorStatusBar.js +++ b/src/editor/EditorStatusBar.js @@ -50,7 +50,8 @@ define(function (require, exports, module) { CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), DocumentManager = require("document/DocumentManager"), - StringUtils = require("utils/StringUtils"); + StringUtils = require("utils/StringUtils"), + HealthLogger = require("utils/HealthLogger"); var SupportedEncodingsText = require("text!supported-encodings.json"), SupportedEncodings = JSON.parse(SupportedEncodingsText); @@ -174,6 +175,13 @@ define(function (require, exports, module) { selStr = ""; if (sels.length > 1) { + //Send analytics data for multicursor use + HealthLogger.sendAnalyticsData( + "multiCursor", + "usage", + "multiCursor", + "use" + ); selStr = StringUtils.format(Strings.STATUSBAR_SELECTION_MULTIPLE, sels.length); } else if (editor.hasSelection()) { var sel = sels[0]; @@ -457,6 +465,18 @@ define(function (require, exports, module) { var document = EditorManager.getActiveEditor().document, fullPath = document.file.fullPath; + var fileType = (document.file instanceof InMemoryFile) ? "newFile" : "existingFile", + filelanguageName = lang ? lang._name : ""; + + HealthLogger.sendAnalyticsData( + HealthLogger.commonStrings.USAGE + HealthLogger.commonStrings.LANGUAGE_CHANGE + + filelanguageName + fileType, + HealthLogger.commonStrings.USAGE, + HealthLogger.commonStrings.LANGUAGE_CHANGE, + filelanguageName.toLowerCase(), + fileType + ); + if (lang === LANGUAGE_SET_AS_DEFAULT) { // Set file's current language in preferences as a file extension override (only enabled if not default already) var fileExtensionMap = PreferencesManager.get("language.fileExtensions"); diff --git a/src/editor/ImageViewer.js b/src/editor/ImageViewer.js index ba24847b739..5043e180d0b 100644 --- a/src/editor/ImageViewer.js +++ b/src/editor/ImageViewer.js @@ -48,7 +48,7 @@ define(function (require, exports, module) { */ function ImageView(file, $container) { this.file = file; - this.$el = $(Mustache.render(ImageViewTemplate, {fullPath: FileUtils.encodeFilePath(file.fullPath), + this.$el = $(Mustache.render(ImageViewTemplate, {fullPath: file.encodedPath || 'file:///' + FileUtils.encodeFilePath(file.fullPath), now: new Date().valueOf()})); $container.append(this.$el); diff --git a/src/extensions/default/AutoUpdate/MessageIds.js b/src/extensions/default/AutoUpdate/MessageIds.js new file mode 100644 index 00000000000..464ddf48836 --- /dev/null +++ b/src/extensions/default/AutoUpdate/MessageIds.js @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + exports.DOWNLOAD_INSTALLER = "node.downloadInstaller"; + exports.PERFORM_CLEANUP = "node.performCleanup"; + exports.VALIDATE_INSTALLER = "node.validateInstaller"; + exports.INITIALIZE_STATE = "node.initializeState"; + exports.CHECK_INSTALLER_STATUS = "node.checkInstallerStatus"; + exports.REMOVE_FROM_REQUESTERS = "node.removeFromRequesters"; + exports.SHOW_STATUS_INFO = "brackets.showStatusInfo"; + exports.NOTIFY_DOWNLOAD_SUCCESS = "brackets.notifyDownloadSuccess"; + exports.SHOW_ERROR_MESSAGE = "brackets.showErrorMessage"; + exports.NOTIFY_DOWNLOAD_FAILURE = "brackets.notifyDownloadFailure"; + exports.NOTIFY_SAFE_TO_DOWNLOAD = "brackets.notifySafeToDownload"; + exports.NOTIFY_INITIALIZATION_COMPLETE = "brackets.notifyinitializationComplete"; + exports.NOTIFY_VALIDATION_STATUS = "brackets.notifyvalidationStatus"; + exports.NOTIFY_INSTALLATION_STATUS = "brackets.notifyInstallationStatus"; + exports.NODE_DOMAIN_INITIALIZED = "brackets.nodeDomainInitialized"; + exports.REGISTER_BRACKETS_FUNCTIONS = "brackets.registerBracketsFunctions"; +}); diff --git a/src/extensions/default/AutoUpdate/StateHandler.js b/src/extensions/default/AutoUpdate/StateHandler.js new file mode 100644 index 00000000000..8cb9e5beae1 --- /dev/null +++ b/src/extensions/default/AutoUpdate/StateHandler.js @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystem = brackets.getModule("filesystem/FileSystem"), + FileUtils = brackets.getModule("file/FileUtils"); + + var FILE_NOT_FOUND = 0, + FILE_NOT_READ = 1, + FILE_PARSE_EXCEPTION = 2, + FILE_READ_FAIL = 3; + + /** + * @constructor + * Creates a StateHandler object for a JSON file. It maintains the following : + * path - path to the JSON file, + * state - parsed content of the file + * @param {string} path - path to the JSON file + */ + var StateHandler = function (path) { + this.path = path; + this.state = null; + }; + + /** + * Checks if the file exists + * @returns {$.Deferred} - a jquery deferred promise, + * that is resolved with existence or non-existence + * of json file. + */ + StateHandler.prototype.exists = function () { + var result = $.Deferred(), + _file = FileSystem.getFileForPath(this.path); + + _file.exists(function (err, exists) { + if (err) { + result.reject(); + } else if (exists) { + result.resolve(); + } else { + result.reject(); + } + }); + + return result.promise(); + }; + + /** + * Parses the JSON file, and maintains a state for the parsed data + * @returns {$.Deferred} - a jquery deferred promise, + * that is resolved with a parsing success or failure + */ + StateHandler.prototype.parse = function () { + var result = $.Deferred(), + _file = FileSystem.getFileForPath(this.path); + var self = this; + + this.exists() + .done(function () { + FileUtils.readAsText(_file) + .done(function (text) { + try { + if (text) { + self.state = JSON.parse(text); + result.resolve(); + } else { + result.reject(FILE_READ_FAIL); + } + } catch (error) { + result.reject(FILE_PARSE_EXCEPTION); + } + }) + .fail(function () { + result.reject(FILE_NOT_READ); + }); + + }) + .fail(function () { + result.reject(FILE_NOT_FOUND); + }); + + return result.promise(); + }; + + /** + * Sets the value of a key in a json file. + * @param {string} key - key for which the value is to be set + * @param {string} value - the value to be set for the given key + * @returns {$.Deferred} - a jquery deferred promise, that is resolved with a write success or failure + */ + StateHandler.prototype.set = function (key, value) { + this.state = this.state || {}; + this.state[key] = value; + + return this.write(true); + }; + + /** + * Gets the value for a given key, from the in-memory state maintained for a json file. + * @param {string} key - key for which value is to be retrieved + * @returns {string} value for the given key + */ + StateHandler.prototype.get = function (key) { + var retval = null; + + if (this.state && this.state[key] !== undefined) { + retval = this.state[key]; + } + + return retval; + }; + + /** + * Reads the value of a key from a json file. + * @param {string} key - key for which the value is to be read + * @returns {$.Deferred} - a jquery deferred promise, that is resolved + * with the read value or read failure. + */ + StateHandler.prototype.refresh = function () { + var result = $.Deferred(), + self = this; + + self.parse() + .done(function () { + if (self.state) { + result.resolve(); + } else { + result.reject(); + console.warn("AutoUpdate : updateHelper.json could not be read"); + } + }) + .fail(function (error) { + result.reject(); + console.error("AutoUpdate : updateHelper.json could not be parsed", error); + }); + + return result.promise(); + }; + + /** + * Performs the write of JSON object to a file. + * @param {string} filepath - path to JSON file + * @param {object} json - JSON object to write + * @returns {$.Deferred} - a jquery deferred promise, + * that is resolved with the write success or failure + */ + function _write(filePath, json) { + var result = $.Deferred(), + _file = FileSystem.getFileForPath(filePath); + + if (_file) { + + var content = JSON.stringify(json); + FileUtils.writeText(_file, content, true) + .done(function () { + result.resolve(); + }) + .fail(function (err) { + result.reject(); + }); + + } else { + result.reject(); + } + + return result.promise(); + } + /** + * Writes content into a json file + * @param {boolean} overwrite - true if file is to be overwritten, false otherwise + * @param {object} [content=this.state] - content to be written into the json file. + * @returns {$.Deferred} - a jquery deferred promise, that is resolved with a write success or failure + */ + StateHandler.prototype.write = function (overwrite) { + var result = $.Deferred(), + self = this; + + function writePromise(path, contentToWrite) { + _write(path, contentToWrite) + .done(function () { + result.resolve(); + }) + .fail(function (err) { + result.reject(); + }); + } + + var content = self.state; + if (overwrite) { + writePromise(self.path, content); + } else { + //check for existence + self.exists() + .fail(function () { + writePromise(self.path, content); + }).done(function () { + result.reject(); + }); + } + + return result.promise(); + }; + + /** + * Resets the content of the in-memory state + */ + StateHandler.prototype.reset = function () { + this.state = null; + }; + + exports.StateHandler = StateHandler; + exports.MessageKeys = { + FILE_NOT_FOUND: FILE_NOT_FOUND, + FILE_NOT_READ: FILE_NOT_READ, + FILE_PARSE_EXCEPTION: FILE_PARSE_EXCEPTION, + FILE_READ_FAIL: FILE_READ_FAIL + }; +}); diff --git a/src/extensions/default/AutoUpdate/UpdateInfoBar.js b/src/extensions/default/AutoUpdate/UpdateInfoBar.js new file mode 100644 index 00000000000..5244fca3079 --- /dev/null +++ b/src/extensions/default/AutoUpdate/UpdateInfoBar.js @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var MainViewManager = brackets.getModule("view/MainViewManager"), + Mustache = brackets.getModule("thirdparty/mustache/mustache"), + EventDispatcher = brackets.getModule("utils/EventDispatcher"), + UpdateBarHtml = require("text!htmlContent/updateBar.html"), + Strings = brackets.getModule("strings"), + _ = brackets.getModule("thirdparty/lodash"); + + EventDispatcher.makeEventDispatcher(exports); + + /** Event triggered when Restart button is clicked on the update bar + */ + var RESTART_BTN_CLICKED = "restartBtnClicked"; + + /** Event triggered when Later button is clicked on the update bar + */ + var LATER_BTN_CLICKED = "laterBtnClicked"; + + // Key handlers for buttons in UI + var SPACE_KEY = 32, // keycode for space key + ESC_KEY = 27; // keycode for escape key + + /** + * Generates the json to be used by Mustache for rendering + * @param {object} msgObj - json object containing message information to be displayed + * @returns {object} - the generated json object + */ + function generateJsonForMustache(msgObj) { + var msgJsonObj = {}; + if (msgObj.type) { + msgJsonObj.type = "'" + msgObj.type + "'"; + } + msgJsonObj.title = msgObj.title; + msgJsonObj.description = msgObj.description; + if (msgObj.needButtons) { + msgJsonObj.buttons = [{ + "id": "restart", + "value": Strings.RESTART_BUTTON, + "tIndex": "'0'" + }, { + "id": "later", + "value": Strings.LATER_BUTTON, + "tIndex": "'0'" + }]; + msgJsonObj.needButtons = msgObj.needButtons; + } + return msgJsonObj; + } + + /** + * Removes and cleans up the update bar from DOM + */ + function cleanUpdateBar() { + var $updateBar = $('#update-bar'); + if ($updateBar.length > 0) { + $updateBar.remove(); + } + $(window.document).off("keydown.AutoUpdate"); + $(window).off('resize.AutoUpdateBar'); + } + + /** + * Displays the Update Bar UI + * @param {object} msgObj - json object containing message info to be displayed + * + */ + function showUpdateBar(msgObj) { + var jsonToMustache = generateJsonForMustache(msgObj), + $updateBarElement = $(Mustache.render(UpdateBarHtml, jsonToMustache)); + + cleanUpdateBar(); //Remove an already existing update bar, if any + $updateBarElement.prependTo(".content"); + + var $updateBar = $('#update-bar'), + $updateContent = $updateBar.find('#update-content'), + $contentContainer = $updateBar.find('#content-container'), + $buttonContainer = $updateBar.find('#button-container'), + $iconContainer = $updateBar.find('#icon-container'), + $closeIconContainer = $updateBar.find('#close-icon-container'), + $heading = $updateBar.find('#heading'), + $description = $updateBar.find('#description'), + $restart = $updateBar.find('#update-btn-restart'), + $later = $updateBar.find('#update-btn-later'), + $closeIcon = $updateBar.find('#close-icon'); + + if ($updateContent.length > 0) { + if ($updateContent[0].scrollWidth > $updateContent.innerWidth()) { + //Text has over-flown, show the update content as tooltip message + if ($contentContainer.length > 0 && + $heading.length > 0 && + $description.length > 0) { + $contentContainer.attr("title", $heading.text() + $description.text()); + } + } + } + // Content Container Width between Icon Container and Button Container or Close Icon Container + // will be assigned when window will be rezied. + var resizeContentContainer = function () { + if($updateContent.length > 0 && $contentContainer.length > 0 && $updateBar.length > 0) { + var newWidth = $updateBar.outerWidth() - 38; + if($buttonContainer.length > 0) { + newWidth = newWidth- $buttonContainer.outerWidth(); + } + if($iconContainer.length > 0) { + newWidth = newWidth - $iconContainer.outerWidth(); + } + if($closeIconContainer.length > 0) { + newWidth = newWidth - $closeIconContainer.outerWidth(); + } + + $contentContainer.css({ + "maxWidth": newWidth + }); + } + }; + + resizeContentContainer(); + $(window).on('resize.AutoUpdateBar', _.debounce(resizeContentContainer, 150)); + + //Event handlers on the Update Bar + + // Click and key handlers on Restart button + if ($restart.length > 0) { + $restart.click(function () { + cleanUpdateBar(); + exports.trigger(exports.RESTART_BTN_CLICKED); + }); + + $restart.keyup(function (event) { + if (event.which === SPACE_KEY) { + $restart.trigger('click'); + } + }); + } + + // Click and key handlers on Later button + if ($later.length > 0) { + $later.click(function () { + cleanUpdateBar(); + MainViewManager.focusActivePane(); + exports.trigger(exports.LATER_BTN_CLICKED); + }); + + $later.keyup(function (event) { + if (event.which === SPACE_KEY) { + $later.trigger('click'); + } + }); + } + + // Click and key handlers on Close button + if ($closeIcon.length > 0) { + $closeIcon.click(function () { + cleanUpdateBar(); + MainViewManager.focusActivePane(); + }); + + $closeIcon.keyup(function (event) { + if (event.which === SPACE_KEY) { + $closeIcon.trigger('click'); + } + }); + } + $(window.document).on("keydown.AutoUpdate", function (event) { + var code = event.which; + if (code === ESC_KEY) { + // Keyboard input of Esc key on Update Bar dismisses and removes the bar + cleanUpdateBar(); + MainViewManager.focusActivePane(); + event.stopImmediatePropagation(); + } + }); + } + exports.showUpdateBar = showUpdateBar; + exports.RESTART_BTN_CLICKED = RESTART_BTN_CLICKED; + exports.LATER_BTN_CLICKED = LATER_BTN_CLICKED; +}); diff --git a/src/extensions/default/AutoUpdate/UpdateStatus.js b/src/extensions/default/AutoUpdate/UpdateStatus.js new file mode 100644 index 00000000000..9eac341ae48 --- /dev/null +++ b/src/extensions/default/AutoUpdate/UpdateStatus.js @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var UpdateStatusHtml = require("text!htmlContent/updateStatus.html"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + Mustache = brackets.getModule("thirdparty/mustache/mustache"), + Strings = brackets.getModule("strings"), + StringUtils = brackets.getModule("utils/StringUtils"); + + ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); + + /** + * Cleans up status information from Status Bar + */ + function cleanUpdateStatus() { + $('#update-status').remove(); + } + + /** + * Displays the status information on Status Bar + * @param {string} id - the id of string to display + */ + function showUpdateStatus(id) { + cleanUpdateStatus(); + + var $updateStatus = $(Mustache.render(UpdateStatusHtml, {"Strings": Strings})); + $updateStatus.appendTo('#status-bar'); + if(id === "initial-download") { + var valStr = StringUtils.format(Strings.NUMBER_WITH_PERCENTAGE, 0); + $('#update-status #' + id + ' #' + 'percent').text(valStr); + } + $('#update-status #' + id).show(); + } + + /** + * Modifies the status information on Status Bar + * @param {object} statusObj - json containing status info - { + * target - id of string to modify, + * spans - array of objects of type - { + * id - id of span for modifiable substring, + * val - the new value to modifiable substring } + * } + */ + function modifyUpdateStatus(statusObj) { + statusObj.spans.forEach(function (span) { + var valStr = span.val; + if(span.id === "percent") { + valStr = StringUtils.format(Strings.NUMBER_WITH_PERCENTAGE, span.val.split('%')[0]); + } + $('#update-status #' + statusObj.target + ' #' + span.id).text(valStr); + }); + } + + /** + * Displays the progress bar on Status bar, while the download is in progress + * @param {object} statusObj - json containing status info - { + * target - id of string to modify, + * spans - array of objects of type - { + * id - id of span for modifiable substring, + * val - the new value to modifiable substring } + * } + */ + function displayProgress(statusObj) { + statusObj.spans.forEach(function (span) { + if (span.id === 'percent') { + var bgColor = $('#status-bar').css('backgroundColor'), + bgval = 'linear-gradient(to right, #1474BF ' + span.val + ', ' + bgColor + ' 0%'; + $('#update-status').css('background', bgval); + } + }); + + } + + exports.showUpdateStatus = showUpdateStatus; + exports.modifyUpdateStatus = modifyUpdateStatus; + exports.cleanUpdateStatus = cleanUpdateStatus; + exports.displayProgress = displayProgress; +}); diff --git a/src/extensions/default/AutoUpdate/htmlContent/updateBar.html b/src/extensions/default/AutoUpdate/htmlContent/updateBar.html new file mode 100644 index 00000000000..9014aac8d21 --- /dev/null +++ b/src/extensions/default/AutoUpdate/htmlContent/updateBar.html @@ -0,0 +1,20 @@ +
+
+ +
+
+

{{title}}  {{{description}}}

+
+ {{#needButtons}} +
+ {{#buttons}} + + {{/buttons}} +
+ {{/needButtons}} + {{^buttons}} +
+ +
+ {{/buttons}} +
\ No newline at end of file diff --git a/src/extensions/default/AutoUpdate/htmlContent/updateStatus.html b/src/extensions/default/AutoUpdate/htmlContent/updateStatus.html new file mode 100644 index 00000000000..88c645f0e16 --- /dev/null +++ b/src/extensions/default/AutoUpdate/htmlContent/updateStatus.html @@ -0,0 +1,5 @@ +
+

{{Strings.INITIAL_DOWNLOAD}}

+

{{Strings.RETRY_DOWNLOAD}}1/5

+

{{Strings.VALIDATING_INSTALLER}}

+
diff --git a/src/extensions/default/AutoUpdate/images/alert.svg b/src/extensions/default/AutoUpdate/images/alert.svg new file mode 100644 index 00000000000..2390583d92f --- /dev/null +++ b/src/extensions/default/AutoUpdate/images/alert.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/extensions/default/AutoUpdate/images/checkmarkcircle.svg b/src/extensions/default/AutoUpdate/images/checkmarkcircle.svg new file mode 100644 index 00000000000..fc0706d5d99 --- /dev/null +++ b/src/extensions/default/AutoUpdate/images/checkmarkcircle.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/extensions/default/AutoUpdate/images/info.svg b/src/extensions/default/AutoUpdate/images/info.svg new file mode 100644 index 00000000000..5b23b6491fd --- /dev/null +++ b/src/extensions/default/AutoUpdate/images/info.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/extensions/default/AutoUpdate/main.js b/src/extensions/default/AutoUpdate/main.js new file mode 100644 index 00000000000..5fd13a0d0c4 --- /dev/null +++ b/src/extensions/default/AutoUpdate/main.js @@ -0,0 +1,1113 @@ +/* + * Copyright (c) 2018 - 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. + * + */ +/* eslint-disable indent */ +/* eslint-disable max-len */ + +define(function (require, exports, module) { + "use strict"; + + var CommandManager = brackets.getModule("command/CommandManager"), + ProjectManager = brackets.getModule("project/ProjectManager"), + Commands = brackets.getModule("command/Commands"), + AppInit = brackets.getModule("utils/AppInit"), + UpdateNotification = brackets.getModule("utils/UpdateNotification"), + DocumentCommandHandlers = brackets.getModule("document/DocumentCommandHandlers"), + NodeDomain = brackets.getModule("utils/NodeDomain"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + FileUtils = brackets.getModule("file/FileUtils"), + Strings = brackets.getModule("strings"), + HealthLogger = brackets.getModule("utils/HealthLogger"), + StateHandlerModule = require("StateHandler"), + MessageIds = require("MessageIds"), + UpdateStatus = require("UpdateStatus"), + UpdateInfoBar = require("UpdateInfoBar"); + + + var _modulePath = FileUtils.getNativeModuleDirectoryPath(module), + _nodePath = "node/AutoUpdateDomain", + _domainPath = [_modulePath, _nodePath].join("/"), + updateDomain, + domainID; + + var appSupportDirectory = brackets.app.getApplicationSupportDirectory(), + updateDir = appSupportDirectory + '/updateTemp', + updateJsonPath = updateDir + '/' + 'updateHelper.json'; + + var StateHandler = StateHandlerModule.StateHandler, + StateHandlerMessages = StateHandlerModule.MessageKeys; + + var updateJsonHandler; + + var MAX_DOWNLOAD_ATTEMPTS = 6, + downloadAttemptsRemaining; + + // Below Strings are to identify an AutoUpdate Event. + var autoUpdateEventNames = { + AUTOUPDATE_DOWNLOAD_START: "DownloadStarted", + AUTOUPDATE_DOWNLOAD_COMPLETED: "DownloadCompleted", + AUTOUPDATE_DOWNLOAD_FAILED: "DownloadFailed", + AUTOUPDATE_DOWNLOAD_COMPLETE_USER_CLICK_RESTART: "DownloadCompletedAndUserClickedRestart", + AUTOUPDATE_DOWNLOAD_COMPLETE_USER_CLICK_LATER: "DownloadCompletedAndUserClickedLater", + AUTOUPDATE_DOWNLOADCOMPLETE_UPDATE_BAR_RENDERED: "DownloadCompleteUpdateBarRendered", + AUTOUPDATE_INSTALLATION_FAILED: "InstallationFailed", + AUTOUPDATE_INSTALLATION_SUCCESS: "InstallationSuccess", + AUTOUPDATE_CLEANUP_FAILED: "AutoUpdateCleanUpFailed" + }; + + // function map for brackets functions + var functionMap = {}; + + var _updateParams; + + //Namespacing the event + var APP_QUIT_CANCELLED = DocumentCommandHandlers.APP_QUIT_CANCELLED + ".auto-update"; + + var _nodeErrorMessages = { + UPDATEDIR_READ_FAILED: 0, + UPDATEDIR_CLEAN_FAILED: 1, + CHECKSUM_DID_NOT_MATCH: 2, + INSTALLER_NOT_FOUND: 3, + DOWNLOAD_ERROR: 4, + NETWORK_SLOW_OR_DISCONNECTED: 5 + }; + + var updateProgressKey = "autoUpdateInProgress", + isAutoUpdateInitiated = false; + + /** + * Checks if an auto update session is currently in progress + * @returns {boolean} - true if an auto update session is currently in progress, false otherwise + */ + function checkIfAnotherSessionInProgress() { + var result = $.Deferred(); + if(updateJsonHandler) { + var state = updateJsonHandler.state; + + updateJsonHandler.refresh() + .done(function() { + var val = updateJsonHandler.get(updateProgressKey); + if(val !== null) { + result.resolve(val); + } else { + result.reject(); + } + }) + .fail(function() { + updateJsonHandler.state = state; + result.reject(); + }); + } + return result.promise(); + } + + /** + * Checks if auto update preference is enabled or disabled + * @private + * @returns {boolean} - true if preference enabled, false otherwise + */ + function _isAutoUpdateEnabled() { + return (PreferencesManager.get("autoUpdate.AutoUpdate") !== false); + } + + /** + * Receives messages from node + * @param {object} event - event received from node + * @param {object} msgObj - json containing - { + * fn - function to execute on Brackets side + * args - arguments to the above function + * requester - ID of the current requester domain + */ + function receiveMessageFromNode(event, msgObj) { + if (functionMap[msgObj.fn]) { + if(domainID === msgObj.requester) { + functionMap[msgObj.fn].apply(null, msgObj.args); + } + } + } + + /* + * Checks if Brackets version got updated + * @returns {boolean} true if version updated, false otherwise + */ + function checkIfVersionUpdated() { + + var latestBuildNumber = updateJsonHandler.get("latestBuildNumber"), + currentBuildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]); + + return latestBuildNumber === currentBuildNumber; + } + + + /** + * Gets the arguments to a function in an array + * @param {object} args - the arguments object + * @returns {Array} - array of actual arguments + */ + function getFunctionArgs(args) { + if (args.length > 1) { + var fnArgs = new Array(args.length - 1), + i; + + for (i = 1; i < args.length; ++i) { + fnArgs[i - 1] = args[i]; + } + return fnArgs; + } + return []; + } + + + /** + * Posts messages to node + * @param {string} messageId - Message to be passed + */ + function postMessageToNode(messageId) { + var msg = { + fn: messageId, + args: getFunctionArgs(arguments), + requester: domainID + }; + updateDomain.exec('data', msg); + } + + /** + * Checks Install failure scenarios + */ + function checkInstallationStatus() { + var searchParams = { + "updateDir": updateDir, + "installErrorStr": ["ERROR:"], + "bracketsErrorStr": ["ERROR:"], + "encoding": "utf8" + }, + // Below are the possible Windows Installer error strings, which will be searched for in the installer logs to track failures. + winInstallErrorStrArr = [ + "Installation success or error status", + "Reconfiguration success or error status" + ]; + if (brackets.platform === "win") { + searchParams.installErrorStr = winInstallErrorStrArr; + searchParams.encoding = "utf16le"; + } + postMessageToNode(MessageIds.CHECK_INSTALLER_STATUS, searchParams); + } + + /** + * Checks and handles the update success and failure scenarios + */ + function checkUpdateStatus() { + var filesToCache = ['.logs'], + downloadCompleted = updateJsonHandler.get("downloadCompleted"), + updateInitiatedInPrevSession = updateJsonHandler.get("updateInitiatedInPrevSession"); + + if (downloadCompleted && updateInitiatedInPrevSession) { + var isNewVersion = checkIfVersionUpdated(); + updateJsonHandler.reset(); + if (isNewVersion) { + // We get here if the update was successful + UpdateInfoBar.showUpdateBar({ + type: "success", + title: Strings.UPDATE_SUCCESSFUL, + description: "" + }); + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_INSTALLATION_SUCCESS, + "autoUpdate", + "install", + "complete", + "" + ); + } else { + // We get here if the update started but failed + checkInstallationStatus(); + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.UPDATE_FAILED, + description: Strings.GO_TO_SITE + }); + } + } else if (downloadCompleted && !updateInitiatedInPrevSession) { + // We get here if the download was complete and user selected UpdateLater + if (brackets.platform === "mac") { + filesToCache = ['.dmg', '.json']; + } else if (brackets.platform === "win") { + filesToCache = ['.msi', '.json']; + } + } + + postMessageToNode(MessageIds.PERFORM_CLEANUP, filesToCache); + } + + /** + * Send Installer Error Code to Analytics Server + */ + + function handleInstallationStatus(statusObj) { + var errorCode = "", + errorline = statusObj.installError; + if (errorline) { + errorCode = errorline.substr(errorline.lastIndexOf(':') + 2, errorline.length); + } + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_INSTALLATION_FAILED, + "autoUpdate", + "install", + "fail", + errorCode + ); + } + + + /** + * Initializes the state of parsed content from updateHelper.json + * returns Promise Object Which is resolved when parsing is success + * and rejected if parsing is failed. + */ + function initState() { + var result = $.Deferred(); + updateJsonHandler.parse() + .done(function() { + result.resolve(); + }) + .fail(function (code) { + var logMsg; + switch (code) { + case StateHandlerMessages.FILE_NOT_FOUND: + logMsg = "AutoUpdate : updateHelper.json cannot be parsed, does not exist"; + break; + case StateHandlerMessages.FILE_NOT_READ: + logMsg = "AutoUpdate : updateHelper.json could not be read"; + break; + case StateHandlerMessages.FILE_PARSE_EXCEPTION: + logMsg = "AutoUpdate : updateHelper.json could not be parsed, exception encountered"; + break; + case StateHandlerMessages.FILE_READ_FAIL: + logMsg = "AutoUpdate : updateHelper.json could not be parsed"; + break; + } + console.log(logMsg); + result.reject(); + }); + return result.promise(); + } + + + + /** + * Sets up the Auto Update environment + */ + function setupAutoUpdate() { + updateJsonHandler = new StateHandler(updateJsonPath); + updateDomain.on('data', receiveMessageFromNode); + + updateDomain.exec('initNode', { + messageIds: MessageIds, + updateDir: updateDir, + requester: domainID + }); + } + + + /** + * Initializes the state for AutoUpdate process + * @returns {$.Deferred} - a jquery promise, + * that is resolved with success or failure + * of state initialization + */ + function initializeState() { + var result = $.Deferred(); + + FileSystem.resolve(updateDir, function (err) { + if (!err) { + result.resolve(); + } else { + var directory = FileSystem.getDirectoryForPath(updateDir); + directory.create(function (error) { + if (error) { + console.error('AutoUpdate : Error in creating update directory in Appdata'); + result.reject(); + } else { + result.resolve(); + } + }); + } + }); + + return result.promise(); + } + + + + /** + * Handles the auto update event, which is triggered when user clicks GetItNow in UpdateNotification dialog + * @param {object} updateParams - json object containing update information { + * installerName - name of the installer + * downloadURL - download URL + * latestBuildNumber - build number + * checksum - checksum } + */ + function initiateAutoUpdate(updateParams) { + _updateParams = updateParams; + downloadAttemptsRemaining = MAX_DOWNLOAD_ATTEMPTS; + + initializeState() + .done(function () { + + var setUpdateInProgress = function() { + var initNodeFn = function () { + isAutoUpdateInitiated = true; + postMessageToNode(MessageIds.INITIALIZE_STATE, _updateParams); + }; + + if (updateJsonHandler.get('latestBuildNumber') !== _updateParams.latestBuildNumber) { + setUpdateStateInJSON('latestBuildNumber', _updateParams.latestBuildNumber) + .done(initNodeFn); + } else { + initNodeFn(); + } + }; + + checkIfAnotherSessionInProgress() + .done(function(inProgress) { + if(inProgress) { + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.AUTOUPDATE_ERROR, + description: Strings.AUTOUPDATE_IN_PROGRESS + }); + } else { + setUpdateStateInJSON("autoUpdateInProgress", true) + .done(setUpdateInProgress); + + } + }) + .fail(function() { + setUpdateStateInJSON("autoUpdateInProgress", true) + .done(setUpdateInProgress); + + }); + + }) + .fail(function () { + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.INITIALISATION_FAILED, + description: "" + }); + }); + } + + + /** + * Typical signature of an update entry, with the most frequently used keys + * @typedef {Object} Update~Entry + * @property {Number} buildNumber - The build number for the update + * @property {string} versionString - Version for the update + * @property {string} releaseNotesURL - URL for release notes for the update + * @property {array} newFeatures - Array of new features in the update + * @property {boolean} prerelease - Boolean to distinguish prerelease from a stable release + * @property {Object} platforms - JSON object, containing asset info for the update, for each platform + * Asset info for each platform consists of : + * @property {string} checksum - checksum of the asset + * @property {string} downloadURL - download URL of the asset + * + */ + + /** + * Handles and processes the update info, required for app auto update + * @private + * @param {Array} updates - array of {...Update~Entry} update entries + */ + function _updateProcessHandler(updates) { + + if (!updates) { + console.warn("AutoUpdate : updates information not available."); + return; + } + var OS = brackets.getPlatformInfo(), + checksum, + downloadURL, + installerName, + platforms, + latestUpdate; + + latestUpdate = updates[0]; + platforms = latestUpdate ? latestUpdate.platforms : null; + + if (platforms && platforms[OS]) { + + //If no checksum field is present then we're setting it to 0, just as a safety check, + // although ideally this situation should never occur in releases post its introduction. + checksum = platforms[OS].checksum ? platforms[OS].checksum : 0, + downloadURL = platforms[OS].downloadURL ? platforms[OS].downloadURL : "", + installerName = downloadURL ? downloadURL.split("/").pop() : ""; + + } else { + // Update not present for current platform + return false; + } + + if (!checksum || !downloadURL || !installerName) { + console.warn("AutoUpdate : asset information incorrect for the update"); + return false; + } + + var updateParams = { + downloadURL: downloadURL, + installerName: installerName, + latestBuildNumber: latestUpdate.buildNumber, + checksum: checksum + }; + + + //Initiate the auto update, with update params + initiateAutoUpdate(updateParams); + + //Send a truthy value to ensure caller is informed about successful initialization of auto-update + return true; + } + + + /** + * Unregisters the App Quit event handler + */ + function resetAppQuitHandler() { + DocumentCommandHandlers.off(APP_QUIT_CANCELLED); + } + + + /** + * Unsets the Auto Update environment + */ + function unsetAutoUpdate() { + updateJsonHandler = null; + updateDomain.off('data'); + resetAppQuitHandler(); + } + + + /** + * Defines preference to enable/disable Auto Update + */ + function setupAutoUpdatePreference() { + PreferencesManager.definePreference("autoUpdate.AutoUpdate", "boolean", true, { + description: Strings.DESCRIPTION_AUTO_UPDATE + }); + + // Set or unset the auto update, based on preference state change + PreferencesManager.on("change", "autoUpdate.AutoUpdate", function () { + if (_isAutoUpdateEnabled()) { + setupAutoUpdate(); + UpdateNotification.registerUpdateHandler(_updateProcessHandler); + } else { + unsetAutoUpdate(); + UpdateNotification.resetToDefaultUpdateHandler(); + } + }); + } + + + /** + * Creates the Node Domain for Auto Update + */ + function setupAutoUpdateDomain() { + updateDomain = new NodeDomain("AutoUpdate", _domainPath); + } + + + /** + * Overriding the appReady for Auto update + */ + + AppInit.appReady(function () { + + // Auto Update is supported on Win and Mac, as of now + if (brackets.platform === "linux" || !(brackets.app.setUpdateParams)) { + return; + } + setupAutoUpdateDomain(); + + //Bail out if update domain could not be created + if (!updateDomain) { + return; + } + + // Check if the update domain is properly initialised + updateDomain.promise() + .done(function () { + setupAutoUpdatePreference(); + if (_isAutoUpdateEnabled()) { + domainID = (new Date()).getTime().toString(); + setupAutoUpdate(); + UpdateNotification.registerUpdateHandler(_updateProcessHandler); + } + }) + .fail(function (err) { + console.error("AutoUpdate : node domain could not be initialized."); + return; + }); + }); + + /** + * Enables/disables the state of "Auto Update In Progress" in UpdateHandler.json + */ + function nodeDomainInitialized(reset) { + initState() + .done(function () { + var inProgress = updateJsonHandler.get(updateProgressKey); + if (inProgress && reset) { + setUpdateStateInJSON(updateProgressKey, !reset) + .always(checkUpdateStatus); + } else if (!inProgress) { + checkUpdateStatus(); + } + }); + } + + + /** + * Enables/disables the state of "Check For Updates" menu entry under Help Menu + */ + function enableCheckForUpdateEntry(enable) { + var cfuCommand = CommandManager.get(Commands.HELP_CHECK_FOR_UPDATE); + cfuCommand.setEnabled(enable); + UpdateNotification.enableUpdateNotificationIcon(enable); + } + + /** + * Checks if it is the first iteration of download + * @returns {boolean} - true if first iteration, false if it is a retrial of download + */ + function isFirstIterationDownload() { + return (downloadAttemptsRemaining === MAX_DOWNLOAD_ATTEMPTS); + } + + /** + * Resets the update state in updatehelper.json in case of failure, + * and logs an error with the message + * @param {string} message - the message to be logged onto console + */ + function resetStateInFailure(message) { + updateJsonHandler.reset(); + + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.UPDATE_FAILED, + description: "" + }); + + enableCheckForUpdateEntry(true); + console.error(message); + + } + + /** + * Sets the update state in updateHelper.json in Appdata + * @param {string} key - key to be set + * @param {string} value - value to be set + * @returns {$.Deferred} - a jquery promise, that is resolved with + * success or failure of state update in json file + */ + function setUpdateStateInJSON(key, value) { + var result = $.Deferred(); + + updateJsonHandler.set(key, value) + .done(function () { + result.resolve(); + }) + .fail(function () { + resetStateInFailure("AutoUpdate : Could not modify updatehelper.json"); + result.reject(); + }); + return result.promise(); + } + + /** + * Handles a safe download of the latest installer, + * safety is ensured by cleaning up any pre-existing installers + * from update directory before beginning a fresh download + */ + function handleSafeToDownload() { + var downloadFn = function () { + if (isFirstIterationDownload()) { + // For the first iteration of download, show download + //status info in Status bar, and pass download to node + UpdateStatus.showUpdateStatus("initial-download"); + postMessageToNode(MessageIds.DOWNLOAD_INSTALLER, true); + } else { + /* For the retry iterations of download, modify the + download status info in Status bar, and pass download to node */ + var attempt = (MAX_DOWNLOAD_ATTEMPTS - downloadAttemptsRemaining); + if (attempt > 1) { + var info = attempt.toString() + "/5"; + var status = { + target: "retry-download", + spans: [{ + id: "attempt", + val: info + }] + }; + UpdateStatus.modifyUpdateStatus(status); + } else { + UpdateStatus.showUpdateStatus("retry-download"); + } + postMessageToNode(MessageIds.DOWNLOAD_INSTALLER, false); + } + + --downloadAttemptsRemaining; + }; + + if(!isAutoUpdateInitiated) { + isAutoUpdateInitiated = true; + updateJsonHandler.refresh() + .done(function() { + setUpdateStateInJSON('downloadCompleted', false) + .done(downloadFn); + }); + } else { + setUpdateStateInJSON('downloadCompleted', false) + .done(downloadFn); + } + } + + /** + * Checks if there is an active internet connection available + * @returns {boolean} - true if online, false otherwise + */ + function checkIfOnline() { + return window.navigator.onLine; + } + + /** + * Attempts a download of the latest installer, while cleaning up any existing downloaded installers + */ + function attemptToDownload() { + if (checkIfOnline()) { + postMessageToNode(MessageIds.PERFORM_CLEANUP, ['.json'], true); + } else { + enableCheckForUpdateEntry(true); + UpdateStatus.cleanUpdateStatus(); + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_FAILED, + "autoUpdate", + "download", + "fail", + "No Internet connection available." + ); + UpdateInfoBar.showUpdateBar({ + type: "warning", + title: Strings.DOWNLOAD_FAILED, + description: Strings.INTERNET_UNAVAILABLE + }); + + setUpdateStateInJSON("autoUpdateInProgress", false); + } + } + + /** + * Validates the checksum of a file against a given checksum + * @param {object} params - json containing { + * filePath - path to the file, + * expectedChecksum - the checksum to validate against } + */ + function validateChecksum(params) { + postMessageToNode(MessageIds.VALIDATE_INSTALLER, params); + } + + /** + * Gets the latest installer, by either downloading a new one or fetching the cached download. + */ + function getLatestInstaller() { + var downloadCompleted = updateJsonHandler.get('downloadCompleted'); + if (!downloadCompleted) { + attemptToDownload(); + } else { + validateChecksum(); + } + } + + /** + * Handles the show status information callback from Node. + * It modifies the info displayed on Status bar. + * @param {object} statusObj - json containing status info { + * target - id of string to display, + * spans - Array containing json objects of type - { + * id - span id, + * val - string to fill the span element with } + * } + */ + function showStatusInfo(statusObj) { + if (statusObj.target === "initial-download") { + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_START, + "autoUpdate", + "download", + "started", + "" + ); + UpdateStatus.modifyUpdateStatus(statusObj); + } + UpdateStatus.displayProgress(statusObj); + } + + /** + * Handles the error messages from Node, in a popup displayed to the user. + * @param {string} message - error string + */ + function showErrorMessage(message) { + var analyticsDescriptionMessage = ""; + + switch (message) { + case _nodeErrorMessages.UPDATEDIR_READ_FAILED: + analyticsDescriptionMessage = "Update directory could not be read."; + break; + case _nodeErrorMessages.UPDATEDIR_CLEAN_FAILED: + analyticsDescriptionMessage = "Update directory could not be cleaned."; + break; + } + console.log("AutoUpdate : Clean-up failed! Reason : " + analyticsDescriptionMessage + ".\n"); + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_CLEANUP_FAILED, + "autoUpdate", + "cleanUp", + "fail", + analyticsDescriptionMessage + ); + } + + + /** + * Handles the Cancel button click by user in + * Unsaved changes prompt, which would come up if user + * has dirty files and he/she clicked UpdateNow + */ + function dirtyFileSaveCancelled() { + UpdateInfoBar.showUpdateBar({ + type: "warning", + title: Strings.WARNING_TYPE, + description: Strings.UPDATE_ON_NEXT_LAUNCH + }); + } + + /** + * Registers the App Quit event handler, in case of dirty + * file save cancelled scenario, while Auto Update is scheduled to run on quit + */ + function setAppQuitHandler() { + resetAppQuitHandler(); + DocumentCommandHandlers.on(APP_QUIT_CANCELLED, dirtyFileSaveCancelled); + } + + + /** + * Initiates the update process, when user clicks UpdateNow in the update popup + * @param {string} formattedInstallerPath - formatted path to the latest installer + * @param {string} formattedLogFilePath - formatted path to the installer log file + * @param {string} installStatusFilePath - path to the install status log file + */ + function initiateUpdateProcess(formattedInstallerPath, formattedLogFilePath, installStatusFilePath) { + + // Get additional update parameters on Mac : installDir, appName, and updateDir + function getAdditionalParams() { + var retval = {}; + var installDir = FileUtils.getNativeBracketsDirectoryPath(); + + if (installDir) { + var appPath = installDir.split("/Contents/www")[0]; + installDir = appPath.substr(0, appPath.lastIndexOf('/')); + var appName = appPath.substr(appPath.lastIndexOf('/') + 1); + + retval = { + installDir: installDir, + appName: appName, + updateDir: updateDir + }; + } + return retval; + } + + // Update function, to carry out app update + var updateFn = function () { + var infoObj = { + installerPath: formattedInstallerPath, + logFilePath: formattedLogFilePath, + installStatusFilePath: installStatusFilePath + }; + + if (brackets.platform === "mac") { + var additionalParams = getAdditionalParams(), + key; + + for (key in additionalParams) { + if (additionalParams.hasOwnProperty(key)) { + infoObj[key] = additionalParams[key]; + } + } + } + + // Set update parameters for app update + if (brackets.app.setUpdateParams) { + brackets.app.setUpdateParams(JSON.stringify(infoObj), function (err) { + if (err) { + resetStateInFailure("AutoUpdate : Update parameters could not be set for the installer. Error encountered: " + err); + } else { + setAppQuitHandler(); + CommandManager.execute(Commands.FILE_QUIT); + } + }); + } else { + resetStateInFailure("AutoUpdate : setUpdateParams could not be found in shell"); + } + }; + setUpdateStateInJSON('updateInitiatedInPrevSession', true) + .done(updateFn); + } + + /** + * Detaches the Update Bar Buttons event handlers + */ + function detachUpdateBarBtnHandlers() { + UpdateInfoBar.off(UpdateInfoBar.RESTART_BTN_CLICKED); + UpdateInfoBar.off(UpdateInfoBar.LATER_BTN_CLICKED); + } + + + /** + * Handles the installer validation callback from Node + * @param {object} statusObj - json containing - { + * valid - (boolean)true for a valid installer, false otherwise, + * installerPath, logFilePath, + * installStatusFilePath - for a valid installer, + * err - for an invalid installer } + */ + function handleValidationStatus(statusObj) { + enableCheckForUpdateEntry(true); + UpdateStatus.cleanUpdateStatus(); + + if (statusObj.valid) { + + // Installer is validated successfully + var statusValidFn = function () { + + // Restart button click handler + var restartBtnClicked = function () { + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_COMPLETE_USER_CLICK_RESTART, + "autoUpdate", + "installNotification", + "installNow ", + "click" + ); + detachUpdateBarBtnHandlers(); + initiateUpdateProcess(statusObj.installerPath, statusObj.logFilePath, statusObj.installStatusFilePath); + }; + + // Later button click handler + var laterBtnClicked = function () { + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_COMPLETE_USER_CLICK_LATER, + "autoUpdate", + "installNotification", + "cancel", + "click" + ); + detachUpdateBarBtnHandlers(); + setUpdateStateInJSON('updateInitiatedInPrevSession', false); + }; + + //attaching UpdateBar handlers + UpdateInfoBar.on(UpdateInfoBar.RESTART_BTN_CLICKED, restartBtnClicked); + UpdateInfoBar.on(UpdateInfoBar.LATER_BTN_CLICKED, laterBtnClicked); + + UpdateInfoBar.showUpdateBar({ + title: Strings.DOWNLOAD_COMPLETE, + description: Strings.CLICK_RESTART_TO_UPDATE, + needButtons: true + }); + + setUpdateStateInJSON("autoUpdateInProgress", false); + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOADCOMPLETE_UPDATE_BAR_RENDERED, + "autoUpdate", + "installNotification", + "render", + "" + ); + }; + + if(!isAutoUpdateInitiated) { + isAutoUpdateInitiated = true; + updateJsonHandler.refresh() + .done(function() { + setUpdateStateInJSON('downloadCompleted', true) + .done(statusValidFn); + }); + } else { + setUpdateStateInJSON('downloadCompleted', true) + .done(statusValidFn); + } + } else { + + // Installer validation failed + + if (updateJsonHandler.get("downloadCompleted")) { + + // If this was a cached download, retry downloading + updateJsonHandler.reset(); + + var statusInvalidFn = function () { + downloadAttemptsRemaining = MAX_DOWNLOAD_ATTEMPTS; + getLatestInstaller(); + }; + + setUpdateStateInJSON('downloadCompleted', false) + .done(statusInvalidFn); + } else { + + // If this is a new download, prompt the message on update bar + var descriptionMessage = "", + analyticsDescriptionMessage = ""; + + switch (statusObj.err) { + case _nodeErrorMessages.CHECKSUM_DID_NOT_MATCH: + descriptionMessage = Strings.CHECKSUM_DID_NOT_MATCH; + analyticsDescriptionMessage = "Checksum didn't match."; + break; + case _nodeErrorMessages.INSTALLER_NOT_FOUND: + descriptionMessage = Strings.INSTALLER_NOT_FOUND; + analyticsDescriptionMessage = "Installer not found."; + break; + } + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_FAILED, + "autoUpdate", + "download", + "fail", + analyticsDescriptionMessage + ); + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.VALIDATION_FAILED, + description: descriptionMessage + }); + + setUpdateStateInJSON("autoUpdateInProgress", false); + } + } + } + + /** + * Handles the download failure callback from Node + * @param {string} message - reason of download failure + */ + function handleDownloadFailure(message) { + console.log("AutoUpdate : Download of latest installer failed in Attempt " + + (MAX_DOWNLOAD_ATTEMPTS - downloadAttemptsRemaining) + ".\n Reason : " + message); + + if (downloadAttemptsRemaining) { + + // Retry the downloading + attemptToDownload(); + } else { + + // Download could not completed, all attempts exhausted + enableCheckForUpdateEntry(true); + UpdateStatus.cleanUpdateStatus(); + + var descriptionMessage = "", + analyticsDescriptionMessage = ""; + if (message === _nodeErrorMessages.DOWNLOAD_ERROR) { + descriptionMessage = Strings.DOWNLOAD_ERROR; + analyticsDescriptionMessage = "Error occurred while downloading."; + } else if (message === _nodeErrorMessages.NETWORK_SLOW_OR_DISCONNECTED) { + descriptionMessage = Strings.NETWORK_SLOW_OR_DISCONNECTED; + analyticsDescriptionMessage = "Network is Disconnected or too slow."; + } + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_FAILED, + "autoUpdate", + "download", + "fail", + analyticsDescriptionMessage + ); + UpdateInfoBar.showUpdateBar({ + type: "error", + title: Strings.DOWNLOAD_FAILED, + description: descriptionMessage + }); + + setUpdateStateInJSON("autoUpdateInProgress", false); + } + + } + + + /** + * Handles the completion of node state initialization + */ + function handleInitializationComplete() { + enableCheckForUpdateEntry(false); + getLatestInstaller(); + } + + + /** + * Handles Download completion callback from Node + */ + function handleDownloadSuccess() { + HealthLogger.sendAnalyticsData( + autoUpdateEventNames.AUTOUPDATE_DOWNLOAD_COMPLETED, + "autoUpdate", + "download", + "complete", + "" + ); + UpdateStatus.showUpdateStatus("validating-installer"); + validateChecksum(); + } + + + function _handleAppClose() { + postMessageToNode(MessageIds.REMOVE_FROM_REQUESTERS); + } + + /** + * Generates a map for brackets side functions + */ + function registerBracketsFunctions() { + functionMap["brackets.notifyinitializationComplete"] = handleInitializationComplete; + functionMap["brackets.showStatusInfo"] = showStatusInfo; + functionMap["brackets.notifyDownloadSuccess"] = handleDownloadSuccess; + functionMap["brackets.showErrorMessage"] = showErrorMessage; + functionMap["brackets.notifyDownloadFailure"] = handleDownloadFailure; + functionMap["brackets.notifySafeToDownload"] = handleSafeToDownload; + functionMap["brackets.notifyvalidationStatus"] = handleValidationStatus; + functionMap["brackets.notifyInstallationStatus"] = handleInstallationStatus; + + ProjectManager.on("beforeProjectClose beforeAppClose", _handleAppClose); + } + functionMap["brackets.nodeDomainInitialized"] = nodeDomainInitialized; + functionMap["brackets.registerBracketsFunctions"] = registerBracketsFunctions; + +}); diff --git a/src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js b/src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js new file mode 100644 index 00000000000..2f63ee3d7e7 --- /dev/null +++ b/src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +/*global exports */ +/*global process*/ +(function () { + "use strict"; + + var _domainManager; + + var request = require('request'), + progress = require('request-progress'), + path = require('path'), + fs = require('fs-extra'), + crypto = require('crypto'); + + // Current Date and Time needed for log filenames + var curDate = Date.now().toString(); + + //AUTOUPDATE_PRERELEASE + //Installer log file + var logFile = curDate + 'update.logs', + logFilePath; + + //Install status file + var installStatusFile = curDate + 'installStatus.logs', + installStatusFilePath; + + var updateDir, + _updateParams; + + var MessageIds, + installerPath; + + // function map for node functions + var functionMap = {}; + + var nodeErrorMessages = { + UPDATEDIR_READ_FAILED: 0, + UPDATEDIR_CLEAN_FAILED: 1, + CHECKSUM_DID_NOT_MATCH: 2, + INSTALLER_NOT_FOUND: 3, + DOWNLOAD_ERROR: 4, + NETWORK_SLOW_OR_DISCONNECTED: 5 + }; + + var requesters = {}, + isNodeDomainInitialized = false; + + /** + * Gets the arguments to a function in an array + * @param {object} args - the arguments object + * @returns {Array} - array of actual arguments + */ + function getFunctionArgs(args) { + if (args.length > 2) { + var fnArgs = new Array(args.length - 2), + i; + for (i = 2; i < args.length; ++i) { + fnArgs[i - 2] = args[i]; + } + return fnArgs; + } + return []; + } + + + /** + * Posts messages to brackets + * @param {string} messageId - Message to be passed + */ + function postMessageToBrackets(messageId, requester) { + if(!requesters[requester]) { + for (var key in requesters) { + requester = key; + break; + } + } + + var msgObj = { + fn: messageId, + args: getFunctionArgs(arguments), + requester: requester.toString() + }; + _domainManager.emitEvent('AutoUpdate', 'data', [msgObj]); + } + + /** + * Quotes and Converts a file path, to accommodate platform dependent paths + * @param {string} qncPath - file path + * @param {boolean} resolve - false if path is only to be quoted, true if both quoted and converted + * @returns {string} quoted and converted file path + */ + function quoteAndConvert(qncPath, resolve) { + if (resolve) { + qncPath = path.resolve(qncPath); + } + return "\"" + qncPath + "\""; + } + + + /** + * Validates the checksum of a file against a given checksum + * @param {object} params - json containing { + * filePath - path to the file, + * expectedChecksum - the checksum to validate against } + */ + function validateChecksum(requester, params) { + params = params || { + filePath: installerPath, + expectedChecksum: _updateParams.checksum + }; + + var hash = crypto.createHash('sha256'), + currentRequester = requester || ""; + + if (fs.existsSync(params.filePath)) { + var stream = fs.createReadStream(params.filePath); + + stream.on('data', function (data) { + hash.update(data); + }); + + stream.on('end', function () { + var calculatedChecksum = hash.digest('hex'), + isValidChecksum = (params.expectedChecksum === calculatedChecksum), + status; + + if (isValidChecksum) { + if (process.platform === "darwin") { + status = { + valid: true, + installerPath: installerPath, + logFilePath: logFilePath, + installStatusFilePath: installStatusFilePath + }; + } else if (process.platform === "win32") { + status = { + valid: true, + installerPath: quoteAndConvert(installerPath, true), + logFilePath: quoteAndConvert(logFilePath, true), + installStatusFilePath: installStatusFilePath + }; + } + } else { + status = { + valid: false, + err: nodeErrorMessages.CHECKSUM_DID_NOT_MATCH + }; + } + postMessageToBrackets(MessageIds.NOTIFY_VALIDATION_STATUS, currentRequester, status); + }); + } else { + var status = { + valid: false, + err: nodeErrorMessages.INSTALLER_NOT_FOUND + }; + postMessageToBrackets(MessageIds.NOTIFY_VALIDATION_STATUS, currentRequester, status); + } + } + + /** + * Parse the Installer log and search for a error strings + * one it finds the line which has any of error String + * it return that line and exit + */ + function parseInstallerLog(filepath, searchstring, encoding, callback) { + var line = ""; + var searchFn = function searchFn(str) { + var arr = str.split('\n'), + lineNum, + pos; + for (lineNum = arr.length - 1; lineNum >= 0; lineNum--) { + var searchStrNum; + for (searchStrNum = 0; searchStrNum < searchstring.length; searchStrNum++) { + pos = arr[lineNum].search(searchstring[searchStrNum]); + if (pos !== -1) { + line = arr[lineNum]; + break; + } + } + if (pos !== -1) { + break; + } + } + callback(line); + }; + + fs.readFile(filepath, {"encoding": encoding}) + .then(function (str) { + return searchFn(str); + }).catch(function () { + callback(""); + }); + } + + /** + * one it finds the line which has any of error String + * after parsing the Log + * it notifies the bracket. + * @param{Object} searchParams is object contains Information Error String + * Encoding of Log File Update Diectory Path. + */ + function checkInstallerStatus(requester, searchParams) { + var installErrorStr = searchParams.installErrorStr, + bracketsErrorStr = searchParams.bracketsErrorStr, + updateDirectory = searchParams.updateDir, + encoding = searchParams.encoding || "utf8", + statusObj = {installError: ": BA_UN"}, + logFileAvailable = false, + currentRequester = requester || ""; + + var notifyBrackets = function notifyBrackets(errorline) { + statusObj.installError = errorline || ": BA_UN"; + postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj); + }; + + var parseLog = function (files) { + files.forEach(function (file) { + var fileExt = path.extname(path.basename(file)); + if (fileExt === ".logs") { + var fileName = path.basename(file), + fileFullPath = updateDirectory + '/' + file; + if (fileName.search("installStatus.logs") !== -1) { + logFileAvailable = true; + parseInstallerLog(fileFullPath, bracketsErrorStr, "utf8", notifyBrackets); + } else if (fileName.search("update.logs") !== -1) { + logFileAvailable = true; + parseInstallerLog(fileFullPath, installErrorStr, encoding, notifyBrackets); + } + } + }); + if (!logFileAvailable) { + postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj); + } + }; + + fs.readdir(updateDirectory) + .then(function (files) { + return parseLog(files); + }).catch(function () { + postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj); + }); + } + + /** + * Downloads the installer for latest Brackets release + * @param {boolean} sendInfo - true if download status info needs to be + * sent back to Brackets, false otherwise + * @param {object} [updateParams=_updateParams] - json containing update parameters + */ + function downloadInstaller(requester, isInitialAttempt, updateParams) { + updateParams = updateParams || _updateParams; + var currentRequester = requester || ""; + try { + var ext = path.extname(updateParams.installerName); + var localInstallerPath = path.resolve(updateDir, Date.now().toString() + ext), + localInstallerFile = fs.createWriteStream(localInstallerPath), + requestCompleted = true, + readTimeOut = 180000; + progress(request(updateParams.downloadURL, {timeout: readTimeOut}), {}) + .on('progress', function (state) { + var target = "retry-download"; + if (isInitialAttempt) { + target = "initial-download"; + } + var info = Math.floor(parseFloat(state.percent) * 100).toString() + '%'; + var status = { + target: target, + spans: [{ + id: "percent", + val: info + }] + }; + postMessageToBrackets(MessageIds.SHOW_STATUS_INFO, currentRequester, status); + }) + .on('error', function (err) { + console.log("AutoUpdate : Download failed. Error occurred : " + err.toString()); + requestCompleted = false; + localInstallerFile.end(); + var error = err.code === 'ESOCKETTIMEDOUT' || err.code === 'ENOTFOUND' ? + nodeErrorMessages.NETWORK_SLOW_OR_DISCONNECTED : + nodeErrorMessages.DOWNLOAD_ERROR; + postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE, currentRequester, error); + }) + .pipe(localInstallerFile) + .on('close', function () { + if (requestCompleted) { + try { + fs.renameSync(localInstallerPath, installerPath); + postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_SUCCESS, currentRequester); + } catch (e) { + console.log("AutoUpdate : Download failed. Exception occurred : " + e.toString()); + postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE, + currentRequester, nodeErrorMessages.DOWNLOAD_ERROR); + } + } + }); + } catch (e) { + console.log("AutoUpdate : Download failed. Exception occurred : " + e.toString()); + postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE, + currentRequester, nodeErrorMessages.DOWNLOAD_ERROR); + } + } + + /** + * Performs clean up for the contents in Update Directory in AppData + * @param {Array} filesToCache - array of file types to cache + * @param {boolean} notifyBack - true if Brackets needs to be + * notified post cleanup, false otherwise + */ + function performCleanup(requester, filesToCache, notifyBack) { + var currentRequester = requester || ""; + function filterFilesAndNotify(files, filesToCacheArr, notifyBackToBrackets) { + files.forEach(function (file) { + var fileExt = path.extname(path.basename(file)); + if (filesToCacheArr.indexOf(fileExt) < 0) { + var fileFullPath = updateDir + '/' + file; + try { + fs.removeSync(fileFullPath); + } catch (e) { + console.log("AutoUpdate : Exception occured in removing ", fileFullPath, e); + } + } + }); + if (notifyBackToBrackets) { + postMessageToBrackets(MessageIds.NOTIFY_SAFE_TO_DOWNLOAD, currentRequester); + } + } + + fs.stat(updateDir) + .then(function (stats) { + if (stats) { + if (filesToCache) { + fs.readdir(updateDir) + .then(function (files) { + filterFilesAndNotify(files, filesToCache, notifyBack); + }) + .catch(function (err) { + console.log("AutoUpdate : Error in Reading Update Dir for Cleanup : " + err.toString()); + postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE, + currentRequester, nodeErrorMessages.UPDATEDIR_READ_FAILED); + }); + } else { + fs.remove(updateDir) + .then(function () { + console.log('AutoUpdate : Update Directory in AppData Cleaned: Complete'); + }) + .catch(function (err) { + console.log("AutoUpdate : Error in Cleaning Update Dir : " + err.toString()); + postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE, + currentRequester, nodeErrorMessages.UPDATEDIR_CLEAN_FAILED); + }); + } + } + }) + .catch(function (err) { + console.log("AutoUpdate : Error in Reading Update Dir stats for Cleanup : " + err.toString()); + postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE, + currentRequester, nodeErrorMessages.UPDATEDIR_CLEAN_FAILED); + }); + } + + /** + * Initializes the node with update parameters + * @param {object} updateParams - json containing update parameters + */ + function initializeState(requester, updateParams) { + var currentRequester = requester || ""; + _updateParams = updateParams; + installerPath = path.resolve(updateDir, updateParams.installerName); + postMessageToBrackets(MessageIds.NOTIFY_INITIALIZATION_COMPLETE, currentRequester); + } + + + function removeFromRequesters(requester) { + if (requesters.hasOwnProperty(requester.toString())) { + delete requesters[requester]; + } + } + + /** + * Generates a map for node side functions + */ + function registerNodeFunctions() { + functionMap["node.downloadInstaller"] = downloadInstaller; + functionMap["node.performCleanup"] = performCleanup; + functionMap["node.validateInstaller"] = validateChecksum; + functionMap["node.initializeState"] = initializeState; + functionMap["node.checkInstallerStatus"] = checkInstallerStatus; + functionMap["node.removeFromRequesters"] = removeFromRequesters; + } + + /** + * Initializes node for the auto update, registers messages and node side funtions + * @param {object} initObj - json containing init information { + * messageIds : Messages for brackets and node communication + * updateDir : update directory in Appdata + * requester : ID of the current requester domain} + */ + function initNode(initObj) { + var resetUpdateProgres = false; + if (!isNodeDomainInitialized) { + MessageIds = initObj.messageIds; + updateDir = path.resolve(initObj.updateDir); + logFilePath = path.resolve(updateDir, logFile); + installStatusFilePath = path.resolve(updateDir, installStatusFile); + registerNodeFunctions(); + isNodeDomainInitialized = true; + resetUpdateProgres = true; + } + postMessageToBrackets(MessageIds.NODE_DOMAIN_INITIALIZED, initObj.requester.toString(), resetUpdateProgres); + requesters[initObj.requester.toString()] = true; + postMessageToBrackets(MessageIds.REGISTER_BRACKETS_FUNCTIONS, initObj.requester.toString()); + } + + + /** + * Receives messages from brackets + * @param {object} msgObj - json containing - { + * fn - function to execute on node side + * args - arguments to the above function } + */ + function receiveMessageFromBrackets(msgObj) { + var argList = msgObj.args; + argList.unshift(msgObj.requester || ""); + functionMap[msgObj.fn].apply(null, argList); + } + + /** + * Initialize the domain with commands and events related to AutoUpdate + * @param {DomainManager} domainManager - The DomainManager for AutoUpdateDomain + */ + + function init(domainManager) { + if (!domainManager.hasDomain("AutoUpdate")) { + domainManager.registerDomain("AutoUpdate", { + major: 0, + minor: 1 + }); + } + _domainManager = domainManager; + + domainManager.registerCommand( + "AutoUpdate", + "initNode", + initNode, + true, + "Initializes node for the auto update", + [ + { + name: "initObj", + type: "object", + description: "json object containing init information" + } + ], + [] + ); + + domainManager.registerCommand( + "AutoUpdate", + "data", + receiveMessageFromBrackets, + true, + "Receives messages from brackets", + [ + { + name: "msgObj", + type: "object", + description: "json object containing message info" + } + ], + [] + ); + + domainManager.registerEvent( + "AutoUpdate", + "data", + [ + { + name: "msgObj", + type: "object", + description: "json object containing message info to pass to brackets" + } + ] + ); + } + + exports.init = init; + +}()); + diff --git a/src/extensions/default/AutoUpdate/node/package.json b/src/extensions/default/AutoUpdate/node/package.json new file mode 100644 index 00000000000..6bf7cd06622 --- /dev/null +++ b/src/extensions/default/AutoUpdate/node/package.json @@ -0,0 +1,8 @@ +{ + "name": "brackets-auto-update", + "dependencies": { + "request": "^2.83.0", + "request-progress": "^3.0.0", + "fs-extra": "^5.0.0" + } +} diff --git a/src/extensions/default/AutoUpdate/styles/styles.css b/src/extensions/default/AutoUpdate/styles/styles.css new file mode 100644 index 00000000000..713d99afad8 --- /dev/null +++ b/src/extensions/default/AutoUpdate/styles/styles.css @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +/*Status bar*/ +#update-status { + position: relative; + float: right; + padding: 0px 20px; + height: 25px; + min-width: 9%; + width: auto; + text-align: center; + background: #fff; +} + +.dark #update-status { + background: #1c1c1e; +} + +#update-status p { + position: relative; + display: none; + white-space: nowrap; + font-family: 'SourceSansPro'; +} + +/*Update Bar*/ +#update-bar { + display: block; + background-color: #105F9C; + box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.53); + height: 38px; + width: 100%; + position: absolute; + z-index: 15; + left: 0px; + bottom: 25px; + outline: none; + overflow: hidden; +} + +#update-bar #icon-container { + width: auto; + height: auto; + padding: 11px; + float: left; +} +#update-bar #icon-container #update-icon { + background: url("../images/info.svg") no-repeat 0 0; + width: 16px; + height: 16px; + display: block; +} + +#update-bar #content-container { + padding: 10px 7px; + float: left; + max-width: 78%; +} + +#update-bar #content-container #update-content { + margin: 0px !important; /*Check if this important is necessary*/ + line-height: 18px; + font-size: 14px; + font-family: 'SourceSansPro'; + color: #FFFFFF; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#update-bar #content-container #update-content #heading{ + font-weight: bold; +} +/*For focussed link of brackets.io*/ +#update-bar #content-container #update-content #description a:focus{ + box-shadow: none; +} + +#update-bar #content-container #update-content #description a{ + text-decoration: underline; + color: #FFFFFF; +} + +#update-bar #button-container { + display: block; + float: right; + right: 40px; + position: fixed; + background-color: #105F9C; + min-width: 180px; +} + +#update-bar #close-icon-container { + height: auto; + padding: 9px; + position: fixed; + float: right; + text-align: center; + width: auto; + min-width: 66px; + right: 30px; + background-color: #105F9C; +} + +#update-bar #close-icon-container #close-icon { + display: block; + color: white; + font-size: 18px; + line-height: 18px; + text-decoration: none; + width: 18px; + height: 18px; + background-color: transparent; + border: none; + padding: 0px; /*This is needed to center the icon*/ + float: right; +} + +#update-bar #close-icon-container #close-icon:hover { + background-color: rgba(255, 255, 255 ,0.16); + border-radius: 50%; +} + +#update-bar #close-icon-container #close-icon:focus { + background-color: rgba(255, 255, 255 ,0.16); + border-radius: 50%; + border: 1px solid #C3E3FF; + outline: 0; +} + +#update-bar #close-icon-container #close-icon:focus:active { + background-color: rgba(255, 255, 255 ,0.32); + border: none; +} + +.update-btn { + width: auto; + height: 28px; + position: relative; + float: right; + padding: 4px 15px; + border: 1px solid #EAEAEA; + border-radius: 3px; + font-size: 14px; + text-align: center; + font-family: 'SourceSansPro'; + color: #E6E6E6; + margin-top: 5px; + margin-right: 10px; + background-color: transparent; +} + +.update-btn:hover { + border-color: #C9C9C9; + background-color: #EAEAEA; + color: #202020; +} + +.update-btn:focus:active { + border: 1px solid #B5B5B5; + background-color: #CCCCCC; + color: #202020; + padding: 4px 15px; + box-shadow: none; +} + +.update-btn:focus { + border: 2px solid #C3E3FF; + background-color: #EAEAEA; + color: #202020; + box-shadow: 0px 3px 6px rgba(148, 206, 255, 0.23); + padding: 3px 14px; +} + +/*Warning Message in Update Bar*/ +#update-bar.warning, #update-bar.warning #close-icon-container { + background-color: #DA7A12; +} + +.dark #update-bar.warning, .dark #update-bar.warning #close-icon-container { + background-color: #E6851A; +} + +#update-bar.warning #icon-container #update-icon, +#update-bar.error #icon-container #update-icon { + background: url("../images/alert.svg") no-repeat 0 0; +} + +/*Error message in Update Bar*/ +#update-bar.error, #update-bar.error #close-icon-container { + background-color: #D7373F; +} + +.dark #update-bar.error, .dark #update-bar.error #close-icon-container{ + background-color: #E4484F; +} +/*Success message in Update Bar*/ +#update-bar.success, #update-bar.success #close-icon-container { + background-color: #278E6B; +} + +.dark #update-bar.success, .dark #update-bar.success #close-icon-container { + background-color: #2E9D77; +} + +#update-bar.success #icon-container #update-icon{ + background: url("../images/checkmarkcircle.svg") no-repeat 0 0; +} + + +/*Overrides*/ + +#status-indicators { + position: relative; + float: right; +} diff --git a/src/extensions/default/CSSCodeHints/CSSProperties.json b/src/extensions/default/CSSCodeHints/CSSProperties.json index 7b80dbca158..342b9875dc9 100644 --- a/src/extensions/default/CSSCodeHints/CSSProperties.json +++ b/src/extensions/default/CSSCodeHints/CSSProperties.json @@ -140,6 +140,8 @@ "image-resolution": {"values": ["from-image", "snap"]}, "isolation": {"values": ["auto", "isolate"]}, "justify-content": {"values": ["center", "flex-end", "flex-start", "space-around", "space-between"]}, + "justify-items": {"values": ["auto", "normal", "stretch", "center", "start", "end", "flex-start", "flex-end", "self-start", "self-end", "left", "right", "baseline", "first", "last", "safe", "unsafe", "legacy", "inherit", "initial"]}, + "justify-self": {"values": ["auto", "normal", "stretch", "center", "start", "end", "flex-start", "flex-end", "self-start", "self-end", "left", "right", "baseline", "first", "last", "safe", "unsafe", "inherit", "initial"]}, "left": {"values": ["auto", "inherit"]}, "letter-spacing": {"values": ["normal", "inherit"]}, "line-height": {"values": ["normal", "inherit"]}, @@ -190,6 +192,7 @@ "resize": {"values": ["both", "horizontal", "none", "vertical", "inherit"]}, "right": {"values": ["auto", "inherit"]}, "scroll-behavior": {"values": ["auto", "smooth"]}, + "scroll-snap-type": {"values": ["none", "x", "y", "block", "inline", "both", "mandatory", "proximity"]}, "src": {"values": [ "url()"]}, "shape-image-threshold": {"values": []}, "shape-inside": {"values": ["auto", "circle()", "ellipse()", "inherit", "outside-shape", "polygon()", "rectangle()"]}, @@ -197,7 +200,7 @@ "shape-outside": {"values": ["none", "inherit", "circle()", "ellipse()", "polygon()", "inset()", "margin-box", "border-box", "padding-box", "content-box", "url()", "image()", "linear-gradient()", "radial-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()"]}, "tab-size": {"values": []}, "table-layout": {"values": ["auto", "fixed", "inherit"]}, - "text-align": {"values": ["center", "left", "justify", "right", "inherit"]}, + "text-align": {"values": ["start", "end", "center", "left", "justify", "right", "match-parent", "justify-all", "inherit"]}, "text-align-last": {"values": ["center", "left", "justify", "right", "inherit"]}, "text-decoration": {"values": ["line-through", "none", "overline", "underline", "inherit"]}, "text-decoration-color": {"values": [], "type": "color"}, @@ -209,6 +212,7 @@ "text-emphasis-position": {"values": ["above", "below", "left", "right"]}, "text-emphasis-style": {"values": ["circle", "dot", "double-circle", "filled", "none", "open", "sesame", "triangle"]}, "text-indent": {"values": ["inherit"]}, + "text-justify": {"values": ["auto", "none", "inter-word", "inter-character", "inherit"]}, "text-overflow": {"values": ["clip", "ellipsis", "inherit"]}, "text-shadow": {"values": []}, "text-rendering": {"values": ["auto", "geometricPrecision", "optimizeLegibility", "optimizeSpeed"]}, diff --git a/src/extensions/default/HealthData/HealthDataManager.js b/src/extensions/default/HealthData/HealthDataManager.js index 519e4182143..e6a72b5a181 100644 --- a/src/extensions/default/HealthData/HealthDataManager.js +++ b/src/extensions/default/HealthData/HealthDataManager.js @@ -129,11 +129,33 @@ define(function (require, exports, module) { return result.promise(); } - // Get Analytics data - function getAnalyticsData() { + /** + *@param{Object} eventParams contails Event Data + * will return complete Analyics Data in Json Format + */ + function getAnalyticsData(eventParams) { var userUuid = PreferencesManager.getViewState("UUID"), olderUuid = PreferencesManager.getViewState("OlderUUID"); + //Create default Values + var defaultEventParams = { + eventCategory: "pingData", + eventSubCategory: "", + eventType: "", + eventSubType: "" + }; + //Override with default values if not present + if (!eventParams) { + eventParams = defaultEventParams; + } else { + var e; + for (e in defaultEventParams) { + if (defaultEventParams.hasOwnProperty(e) && !eventParams[e]) { + eventParams[e] = defaultEventParams[e]; + } + } + } + return { project: brackets.config.serviceKey, environment: brackets.config.environment, @@ -143,10 +165,10 @@ define(function (require, exports, module) { "event.guid": uuid.v4(), "event.user_guid": olderUuid || userUuid, "event.dts_end": new Date().toISOString(), - "event.category": "pingData", - "event.subcategory": "", - "event.type": "", - "event.subtype": "", + "event.category": eventParams.eventCategory, + "event.subcategory": eventParams.eventSubCategory, + "event.type": eventParams.eventType, + "event.subtype": eventParams.eventSubType, "event.user_agent": window.navigator.userAgent || "", "event.language": brackets.app.language, "source.name": brackets.metadata.version, @@ -190,10 +212,10 @@ define(function (require, exports, module) { } // Send Analytics data to Server - function sendAnalyticsDataToServer() { + function sendAnalyticsDataToServer(eventParams) { var result = new $.Deferred(); - var analyticsData = getAnalyticsData(); + var analyticsData = getAnalyticsData(eventParams); $.ajax({ url: brackets.config.analyticsDataServerURL, type: "POST", @@ -270,6 +292,55 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Check if the Analytic Data is to be sent to the server. + * If the user has enabled tracking, Analytic Data will be sent once per session + * Send Analytic Data to the server if the Data associated with the given Event is not yet sent in this session. + * We are sending the data as soon as the user triggers the event. + * The data will be sent to the server only after the notification dialog + * for opt-out/in is closed. + * @param{Object} event event object + * @param{Object} Eventparams Object Containg Data to be sent to Server + * @param{boolean} forceSend Flag for sending analytics data for testing purpose + **/ + function checkAnalyticsDataSend(event, Eventparams, forceSend) { + var result = new $.Deferred(), + isHDTracking = prefs.get("healthDataTracking"), + isEventDataAlreadySent; + + if (isHDTracking) { + isEventDataAlreadySent = HealthLogger.analyticsEventMap.get(Eventparams.eventName); + HealthLogger.analyticsEventMap.set(Eventparams.eventName, true); + if (!isEventDataAlreadySent || forceSend) { + sendAnalyticsDataToServer(Eventparams) + .done(function () { + HealthLogger.analyticsEventMap.set(Eventparams.eventName, true); + result.resolve(); + }).fail(function () { + HealthLogger.analyticsEventMap.set(Eventparams.eventName, false); + result.reject(); + }); + } else { + result.reject(); + } + } else { + result.reject(); + } + + return result.promise(); + } + + /** + * This function is auto called after 24 hours to empty the map + * Map is used to make sure that we send an event only once per 24 hours + **/ + + function emptyAnalyticsMap() { + HealthLogger.analyticsEventMap.clear(); + setTimeout(emptyAnalyticsMap, ONE_DAY); + } + setTimeout(emptyAnalyticsMap, ONE_DAY); + // Expose a command to test data sending capability, but limit it to dev environment only CommandManager.register("Sends health data and Analytics data for testing purpose", "sendHealthData", function() { if (brackets.config.environment === "stage") { @@ -283,6 +354,8 @@ define(function (require, exports, module) { checkHealthDataSend(); }); + HealthLogger.on("SendAnalyticsData", checkAnalyticsDataSend); + window.addEventListener("online", function () { checkHealthDataSend(); }); diff --git a/src/extensions/default/HealthData/HealthDataPreview.js b/src/extensions/default/HealthData/HealthDataPreview.js index 3c7e6f8662a..c84ccfa0e09 100644 --- a/src/extensions/default/HealthData/HealthDataPreview.js +++ b/src/extensions/default/HealthData/HealthDataPreview.js @@ -45,7 +45,7 @@ define(function (require, exports, module) { var result = new $.Deferred(); HealthDataManager.getHealthData().done(function (healthDataObject) { - var combinedHealthAnalyticsData = HealthDataManager.getAnalyticsData("pingData", "", "", ""), + var combinedHealthAnalyticsData = HealthDataManager.getAnalyticsData(), content; combinedHealthAnalyticsData = [healthDataObject, combinedHealthAnalyticsData ]; content = JSON.stringify(combinedHealthAnalyticsData, null, 4); diff --git a/src/extensions/default/InAppNotifications/htmlContent/notificationContainer.html b/src/extensions/default/InAppNotifications/htmlContent/notificationContainer.html new file mode 100644 index 00000000000..b420dee0e70 --- /dev/null +++ b/src/extensions/default/InAppNotifications/htmlContent/notificationContainer.html @@ -0,0 +1,7 @@ +
+
+
+
+ +
+
diff --git a/src/extensions/default/InAppNotifications/main.js b/src/extensions/default/InAppNotifications/main.js new file mode 100644 index 00000000000..b4c2e582c96 --- /dev/null +++ b/src/extensions/default/InAppNotifications/main.js @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +/** + * module for displaying in-app notifications + * + */ +define(function (require, exports, module) { + "use strict"; + + var AppInit = brackets.getModule("utils/AppInit"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + ExtensionManager = brackets.getModule("extensibility/ExtensionManager"), + HealthLogger = brackets.getModule("utils/HealthLogger"), + NotificationBarHtml = require("text!htmlContent/notificationContainer.html"); + + ExtensionUtils.loadStyleSheet(module, "styles/styles.css"); + + // duration of one day in milliseconds + var ONE_DAY = 1000 * 60 * 60 * 24; + + // Init default last notification number + PreferencesManager.stateManager.definePreference("lastHandledNotificationNumber", "number", 0); + + // Init default last info URL fetch time + PreferencesManager.stateManager.definePreference("lastNotificationURLFetchTime", "number", 0); + + /** + * Constructs notification info URL for XHR + * + * @param {string=} localeParam - optional locale, defaults to 'brackets.getLocale()' when omitted. + * @returns {string} the new notification info url + */ + function _getVersionInfoUrl(localeParam) { + + var locale = localeParam || brackets.getLocale(); + + if (locale.length > 2) { + locale = locale.substring(0, 2); + } + + return brackets.config.notification_info_url.replace("", locale); + } + + /** + * Get a data structure that has information for all Brackets targeted notifications. + * + * _notificationInfoUrl is used for unit testing. + */ + function _getNotificationInformation(_notificationInfoUrl) { + // Last time the versionInfoURL was fetched + var lastInfoURLFetchTime = PreferencesManager.getViewState("lastNotificationURLFetchTime"); + + var result = new $.Deferred(); + var fetchData = false; + var data; + + // If we don't have data saved in prefs, fetch + data = PreferencesManager.getViewState("notificationInfo"); + if (!data) { + fetchData = true; + } + + // If more than 24 hours have passed since our last fetch, fetch again + if (Date.now() > lastInfoURLFetchTime + ONE_DAY) { + fetchData = true; + } + + if (fetchData) { + var lookupPromise = new $.Deferred(), + localNotificationInfoUrl; + + // If the current locale isn't "en" or "en-US", check whether we actually have a + // locale-specific notification target, and fall back to "en" if not. + var locale = brackets.getLocale().toLowerCase(); + if (locale !== "en" && locale !== "en-us") { + localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl(); + // Check if we can reach a locale specific notifications source + $.ajax({ + url: localNotificationInfoUrl, + cache: false, + type: "HEAD" + }).fail(function (jqXHR, status, error) { + // Fallback to "en" locale + localNotificationInfoUrl = _getVersionInfoUrl("en"); + }).always(function (jqXHR, status, error) { + lookupPromise.resolve(); + }); + } else { + localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl("en"); + lookupPromise.resolve(); + } + + lookupPromise.done(function () { + $.ajax({ + url: localNotificationInfoUrl, + dataType: "json", + cache: false + }).done(function (notificationInfo, textStatus, jqXHR) { + lastInfoURLFetchTime = (new Date()).getTime(); + PreferencesManager.setViewState("lastNotificationURLFetchTime", lastInfoURLFetchTime); + PreferencesManager.setViewState("notificationInfo", notificationInfo); + result.resolve(notificationInfo); + }).fail(function (jqXHR, status, error) { + // When loading data for unit tests, the error handler is + // called but the responseText is valid. Try to use it here, + // but *don't* save the results in prefs. + + if (!jqXHR.responseText) { + // Text is NULL or empty string, reject(). + result.reject(); + return; + } + + try { + data = JSON.parse(jqXHR.responseText); + result.resolve(data); + } catch (e) { + result.reject(); + } + }); + }); + } else { + result.resolve(data); + } + + return result.promise(); + } + + + /** + * Check for notifications, notification overlays are always displayed + * + * @return {$.Promise} jQuery Promise object that is resolved or rejected after the notification check is complete. + */ + function checkForNotification(versionInfoUrl) { + var result = new $.Deferred(); + + _getNotificationInformation(versionInfoUrl) + .done(function (notificationInfo) { + // Get all available notifications + var notifications = notificationInfo.notifications; + if (notifications && notifications.length > 0) { + // Iterate through notifications and act only on the most recent + // applicable notification + notifications.every(function(notificationObj) { + // Only show the notification overlay if the user hasn't been + // alerted of this notification + if (_checkNotificationValidity(notificationObj)) { + if (notificationObj.silent) { + // silent notifications, to gather user validity based on filters + HealthLogger.sendAnalyticsData("notification", notificationObj.sequence, "handled"); + } else { + showNotification(notificationObj); + } + // Break, we have acted on one notification already + return false; + } + // Continue, we haven't yet got a notification to act on + return true; + }); + } + result.resolve(); + }) + .fail(function () { + // Error fetching the update data. If this is a forced check, alert the user + result.reject(); + }); + + return result.promise(); + } + + function _checkPlatform(filters, _platform) { + return !filters.platforms || filters.platforms.length === 0 || filters.platforms.indexOf(_platform) >=0; + } + + function _checkBuild(filters, _build) { + return !filters.builds || filters.builds.length === 0 || filters.builds.indexOf(_build) >=0; + } + + function _checkVersion(filters, _version) { + var re = new RegExp(filters.version); + return re.exec(_version); + } + + function _checkLocale(filters, _locale) { + return !filters.locales || filters.locales.length === 0 || filters.locales.indexOf(_locale) >=0; + } + + function _checkExpiry(expiry) { + return Date.now() <= expiry; + } + + function _checkExtensions(filters) { + //if no property called extensions then it's a universal notification + if (filters.extensions === undefined) { + return true; + } + + var allExtensions = ExtensionManager.extensions, + allExtnsMatched = true, + userExtensionKeys = Object.keys(allExtensions).filter(function(k) { + return allExtensions[k].installInfo.locationType === 'user'; + }); + + if (!filters.extensions) { + //if property called extensions exists but has a falsy value + //then number of user extensions must be zero + allExtnsMatched = userExtensionKeys.length === 0; + } else if (filters.extensions.length === 0) { + //if property called extensions exists but is an empty array + //then number of user extensions must greater than zero + allExtnsMatched = userExtensionKeys.length > 0; + } else { + //if property called extensions exists but is a non empty array + //then notification is targetted to users having the fitered extensions + var filteredExtns = filters.extensions, + extnIterator = null; + for (var i=0; i < filteredExtns.length; i++) { + extnIterator = filteredExtns[i]; + if (userExtensionKeys.indexOf(extnIterator) === -1) { + allExtnsMatched = false; + break; + } + } + } + return allExtnsMatched; + } + + function _checkNotificationValidity(notificationObj) { + + var filters = notificationObj.filters, + _platform = brackets.getPlatformInfo(), + _locale = brackets.getLocale(), + _lastHandledNotificationNumber = PreferencesManager.getViewState("lastHandledNotificationNumber"), + // Extract current build number from package.json version field 0.0.0-0 + _buildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]), + _version = brackets.metadata.apiVersion; + + if(_locale.length > 2) { + _locale = _locale.substring(0, 2); + } + + return notificationObj.sequence > _lastHandledNotificationNumber + && _checkExpiry(notificationObj.expiry) + && _checkPlatform(filters, _platform) + && _checkLocale(filters, _locale) + && _checkVersion(filters, _version) + && _checkBuild(filters, _buildNumber) + && _checkExtensions(filters); + } + + + /** + * Removes and cleans up the notification bar from DOM + */ + function cleanNotificationBar() { + var $notificationBar = $('#notification-bar'); + if ($notificationBar.length > 0) { + $notificationBar.remove(); + } + } + + /** + * Displays the Notification Bar UI + * @param {object} msgObj - json object containing message info to be displayed + * + */ + function showNotification(msgObj) { + var $htmlContent = $(msgObj.html), + $notificationBarElement = $(NotificationBarHtml); + + // Remove any SCRIPT tag to avoid secuirity issues + $htmlContent.find('script').remove(); + + // Remove any STYLE tag to avoid styling impact on Brackets DOM + $htmlContent.find('style').remove(); + + cleanNotificationBar(); //Remove an already existing notification bar, if any + $notificationBarElement.prependTo(".content"); + + var $notificationBar = $('#notification-bar'), + $notificationContent = $notificationBar.find('.content-container'), + $closeIcon = $notificationBar.find('.close-icon'); + + $notificationContent.append($htmlContent); + HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "shown"); + + // Click handlers on actionable elements + if ($closeIcon.length > 0) { + $closeIcon.click(function () { + cleanNotificationBar(); + PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence); + HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedByClose"); + }); + } + + if (msgObj.actionables) { + $(msgObj.actionables).click(function () { + cleanNotificationBar(); + PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence); + HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedBy" + this.id); + }); + } + } + + + AppInit.appReady(function () { + checkForNotification(); + }); + + // For unit tests only + exports.checkForNotification = checkForNotification; +}); diff --git a/src/extensions/default/InAppNotifications/styles/styles.css b/src/extensions/default/InAppNotifications/styles/styles.css new file mode 100644 index 00000000000..fd843aacb79 --- /dev/null +++ b/src/extensions/default/InAppNotifications/styles/styles.css @@ -0,0 +1,56 @@ +#notification-bar { + display: block; + background-color: #105F9C; + box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.53); + padding: 5px 0px; + width: 100%; + min-height: 39px; + position: absolute; + z-index: 16; + left: 0px; + bottom: 25px; + outline: none; + overflow: hidden; + color: rgb(51, 51, 51); + background: rgb(223, 226, 226); +} + +.dark #notification-bar { + color: #ccc; + background: #2c2c2c; +} + +#notification-bar .content-container { + padding: 5px 10px; + float: left; + width: 100%; +} + +#notification-bar .close-icon-container { + height: auto; + position: absolute; + float: right; + text-align: center; + width: auto; + min-width: 66px; + right: 20px; + top: 10px; +} + +#notification-bar .close-icon-container .close-icon { + display: block; + font-size: 18px; + line-height: 18px; + text-decoration: none; + width: 18px; + height: 18px; + background-color: transparent; + border: none; + padding: 0px; /*This is needed to center the icon*/ + float: right; +} + +.dark #notification-bar .close-icon-container .close-icon { + color: #ccc; +} + diff --git a/src/extensions/default/JavaScriptCodeHints/ParameterHintManager.js b/src/extensions/default/JavaScriptCodeHints/ParameterHintManager.js deleted file mode 100644 index ce84b9cea7d..00000000000 --- a/src/extensions/default/JavaScriptCodeHints/ParameterHintManager.js +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright (c) 2013 - 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. - * - */ - -define(function (require, exports, module) { - "use strict"; - - var _ = brackets.getModule("thirdparty/lodash"); - - var Commands = brackets.getModule("command/Commands"), - CommandManager = brackets.getModule("command/CommandManager"), - KeyEvent = brackets.getModule("utils/KeyEvent"), - Menus = brackets.getModule("command/Menus"), - Strings = brackets.getModule("strings"), - HintsUtils2 = require("HintUtils2"), - ScopeManager = brackets.getModule("JSUtils/ScopeManager"); - - - /** @const {string} Show Function Hint command ID */ - var SHOW_PARAMETER_HINT_CMD_ID = "showParameterHint", // string must MATCH string in native code (brackets_extensions) - PUSH_EXISTING_HINT = true, - OVERWRITE_EXISTING_HINT = false, - hintContainerHTML = require("text!ParameterHintTemplate.html"), - KeyboardPrefs = JSON.parse(require("text!keyboard.json")); - - var $hintContainer, // function hint container - $hintContent, // function hint content holder - - /** @type {{inFunctionCall: boolean, functionCallPos: {line: number, ch: number}, - * fnType: Array.") - .append(_.escape(param)) - .addClass("current-parameter")); - } else { - $hintContent.append(_.escape(param)); - } - } - - if (hints.parameters.length > 0) { - HintsUtils2.formatParameterHint(hints.parameters, appendSeparators, appendParameter); - } else { - $hintContent.append(_.escape(Strings.NO_ARGUMENTS)); - } - } - - /** - * Save the state of the current hint. Called when popping up a parameter hint - * for a parameter, when the parameter already part of an existing parameter - * hint. - */ - function pushHintOnStack() { - hintStack.push(hintState); - } - - /** - * Restore the state of the previous function hint. - * - * @return {boolean} - true the a parameter hint has been popped, false otherwise. - */ - function popHintFromStack() { - if (hintStack.length > 0) { - hintState = hintStack.pop(); - hintState.visible = false; - return true; - } - - return false; - } - - /** - * Reset the function hint stack. - */ - function clearFunctionHintStack() { - hintStack = []; - } - - /** - * Test if the function call at the cursor is different from the currently displayed - * function hint. - * - * @param {{line:number, ch:number}} functionCallPos - the offset of the function call. - * @return {boolean} - */ - function hasFunctionCallPosChanged(functionCallPos) { - var oldFunctionCallPos = hintState.functionCallPos; - return (oldFunctionCallPos === undefined || - oldFunctionCallPos.line !== functionCallPos.line || - oldFunctionCallPos.ch !== functionCallPos.ch); - } - - /** - * Dismiss the function hint. - * - */ - function dismissHint() { - - if (hintState.visible) { - $hintContainer.hide(); - $hintContent.empty(); - hintState = {}; - session.editor.off("cursorActivity", handleCursorActivity); - - if (!preserveHintStack) { - clearFunctionHintStack(); - } - } - } - - /** - * Pop up a function hint on the line above the caret position. - * - * @param {boolean=} pushExistingHint - if true, push the existing hint on the stack. Default is false, not - * to push the hint. - * @param {string=} hint - function hint string from tern. - * @param {{inFunctionCall: boolean, functionCallPos: - * {line: number, ch: number}}=} functionInfo - - * if the functionInfo is already known, it can be passed in to avoid - * figuring it out again. - * @return {jQuery.Promise} - The promise will not complete until the - * hint has completed. Returns null, if the function hint is already - * displayed or there is no function hint at the cursor. - * - */ - function popUpHint(pushExistingHint, hint, functionInfo) { - - functionInfo = functionInfo || session.getFunctionInfo(); - if (!functionInfo.inFunctionCall) { - dismissHint(); - return null; - } - - if (hasFunctionCallPosChanged(functionInfo.functionCallPos)) { - - var pushHint = pushExistingHint && isHintDisplayed(); - if (pushHint) { - pushHintOnStack(); - preserveHintStack = true; - } - - dismissHint(); - preserveHintStack = false; - } else if (isHintDisplayed()) { - return null; - } - - hintState.functionCallPos = functionInfo.functionCallPos; - - var request = null; - var $deferredPopUp = $.Deferred(); - - if (!hint) { - request = ScopeManager.requestParameterHint(session, functionInfo.functionCallPos); - } else { - session.setFnType(hint); - request = $.Deferred(); - request.resolveWith(null, [hint]); - $deferredPopUp.resolveWith(null); - } - - request.done(function (fnType) { - var cm = session.editor._codeMirror, - pos = cm.charCoords(functionInfo.functionCallPos); - - formatHint(functionInfo); - - $hintContainer.show(); - positionHint(pos.left, pos.top, pos.bottom); - hintState.visible = true; - hintState.fnType = fnType; - - session.editor.on("cursorActivity", handleCursorActivity); - $deferredPopUp.resolveWith(null); - }).fail(function () { - hintState = {}; - }); - - return $deferredPopUp; - } - - /** - * Pop up a function hint on the line above the caret position if the character before - * the current cursor is an open parenthesis - * - * @return {jQuery.Promise} - The promise will not complete until the - * hint has completed. Returns null, if the function hint is already - * displayed or there is no function hint at the cursor. - */ - function popUpHintAtOpenParen() { - var functionInfo = session.getFunctionInfo(); - if (functionInfo.inFunctionCall) { - var token = session.getToken(); - - if (token && token.string === "(") { - return popUpHint(); - } - } else { - dismissHint(); - } - - return null; - } - - /** - * Show the parameter the cursor is on in bold when the cursor moves. - * Dismiss the pop up when the cursor moves off the function. - */ - handleCursorActivity = function () { - var functionInfo = session.getFunctionInfo(); - - if (functionInfo.inFunctionCall) { - // If in a different function hint, then dismiss the old one and - // display the new one if there is one on the stack - if (hasFunctionCallPosChanged(functionInfo.functionCallPos)) { - if (popHintFromStack()) { - var poppedFunctionCallPos = hintState.functionCallPos, - currentFunctionCallPos = functionInfo.functionCallPos; - - if (poppedFunctionCallPos.line === currentFunctionCallPos.line && - poppedFunctionCallPos.ch === currentFunctionCallPos.ch) { - preserveHintStack = true; - popUpHint(OVERWRITE_EXISTING_HINT, - hintState.fnType, functionInfo); - preserveHintStack = false; - return; - } - } else { - dismissHint(); - } - } - - formatHint(functionInfo); - return; - } - - dismissHint(); - }; - - /** - * Enable cursor tracking in the current session. - * - * @param {Session} session - session to start cursor tracking on. - */ - function startCursorTracking(session) { - session.editor.on("cursorActivity", handleCursorActivity); - } - - /** - * Stop cursor tracking in the current session. - * - * Use this to move the cursor without changing the function hint state. - * - * @param {Session} session - session to stop cursor tracking on. - */ - function stopCursorTracking(session) { - session.editor.off("cursorActivity", handleCursorActivity); - } - - /** - * Show a parameter hint in its own pop-up. - * - */ - function handleShowParameterHint() { - - // Pop up function hint - popUpHint(); - } - - /** - * Install function hint listeners. - * - * @param {Editor} editor - editor context on which to listen for - * changes - */ - function installListeners(editor) { - editor.on("keydown.ParameterHints", function (event, editor, domEvent) { - if (domEvent.keyCode === KeyEvent.DOM_VK_ESCAPE) { - dismissHint(); - } - }).on("scroll.ParameterHints", function () { - dismissHint(); - }); - } - - /** - * Clean up after installListeners() - * @param {!Editor} editor - */ - function uninstallListeners(editor) { - editor.off(".ParameterHints"); - } - - /** - * Add the function hint command at start up. - */ - function addCommands() { - /* Register the command handler */ - CommandManager.register(Strings.CMD_SHOW_PARAMETER_HINT, SHOW_PARAMETER_HINT_CMD_ID, handleShowParameterHint); - - // Add the menu items - var menu = Menus.getMenu(Menus.AppMenuBar.EDIT_MENU); - if (menu) { - menu.addMenuItem(SHOW_PARAMETER_HINT_CMD_ID, KeyboardPrefs.showParameterHint, Menus.AFTER, Commands.SHOW_CODE_HINTS); - } - - // Close the function hint when commands are executed, except for the commands - // to show function hints for code hints. - CommandManager.on("beforeExecuteCommand", function (event, commandId) { - if (commandId !== SHOW_PARAMETER_HINT_CMD_ID && - commandId !== Commands.SHOW_CODE_HINTS) { - dismissHint(); - } - }); - } - - // Create the function hint container - $hintContainer = $(hintContainerHTML).appendTo($("body")); - $hintContent = $hintContainer.find(".function-hint-content"); - - exports.PUSH_EXISTING_HINT = PUSH_EXISTING_HINT; - exports.addCommands = addCommands; - exports.dismissHint = dismissHint; - exports.installListeners = installListeners; - exports.uninstallListeners = uninstallListeners; - exports.isHintDisplayed = isHintDisplayed; - exports.popUpHint = popUpHint; - exports.popUpHintAtOpenParen = popUpHintAtOpenParen; - exports.setSession = setSession; - exports.startCursorTracking = startCursorTracking; - exports.stopCursorTracking = stopCursorTracking; - -}); diff --git a/src/extensions/default/JavaScriptCodeHints/ParameterHintTemplate.html b/src/extensions/default/JavaScriptCodeHints/ParameterHintTemplate.html deleted file mode 100644 index 04f8a9c04a2..00000000000 --- a/src/extensions/default/JavaScriptCodeHints/ParameterHintTemplate.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
-
-
diff --git a/src/extensions/default/JavaScriptCodeHints/ParameterHintsProvider.js b/src/extensions/default/JavaScriptCodeHints/ParameterHintsProvider.js new file mode 100644 index 00000000000..72c9d27ac73 --- /dev/null +++ b/src/extensions/default/JavaScriptCodeHints/ParameterHintsProvider.js @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2013 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var ScopeManager = brackets.getModule("JSUtils/ScopeManager"), + OVERWRITE_EXISTING_HINT = false; + + function JSParameterHintsProvider() { + this.hintState = {}; + this.hintStack = []; + this.preserveHintStack = null; // close a function hint without clearing stack + this.session = null; // current editor session, updated by main + } + + /** + * Update the current session for use by the Function Hint Manager. + * + * @param {Session} value - current session. + */ + JSParameterHintsProvider.prototype.setSession = function (value) { + this.session = value; + }; + + /** + * Test if a function hint is being displayed. + * + * @return {boolean} - true if a function hint is being displayed, false + * otherwise. + */ + JSParameterHintsProvider.prototype.isHintDisplayed = function () { + return this.hintState.visible === true; + }; + + /** + * Save the state of the current hint. Called when popping up a parameter hint + * for a parameter, when the parameter already part of an existing parameter + * hint. + */ + JSParameterHintsProvider.prototype.pushHintOnStack = function () { + this.hintStack.push(this.hintState); + }; + + /** + * Restore the state of the previous function hint. + * + * @return {boolean} - true the a parameter hint has been popped, false otherwise. + */ + JSParameterHintsProvider.prototype.popHintFromStack = function () { + if (this.hintStack.length > 0) { + this.hintState = this.hintStack.pop(); + this.hintState.visible = false; + return true; + } + + return false; + }; + + /** + * Reset the function hint stack. + */ + JSParameterHintsProvider.prototype.clearFunctionHintStack = function () { + this.hintStack = []; + }; + + /** + * Test if the function call at the cursor is different from the currently displayed + * function hint. + * + * @param {{line:number, ch:number}} functionCallPos - the offset of the function call. + * @return {boolean} + */ + JSParameterHintsProvider.prototype.hasFunctionCallPosChanged = function (functionCallPos) { + var oldFunctionCallPos = this.hintState.functionCallPos; + return (oldFunctionCallPos === undefined || + oldFunctionCallPos.line !== functionCallPos.line || + oldFunctionCallPos.ch !== functionCallPos.ch); + }; + + /** + * Dismiss the function hint. + * + */ + JSParameterHintsProvider.prototype.cleanHintState = function () { + if (this.hintState.visible) { + if (!this.preserveHintStack) { + this.clearFunctionHintStack(); + } + } + }; + + /** + * Pop up a function hint on the line above the caret position. + * + * @param {boolean=} pushExistingHint - if true, push the existing hint on the stack. Default is false, not + * to push the hint. + * @param {string=} hint - function hint string from tern. + * @param {{inFunctionCall: boolean, functionCallPos: + * {line: number, ch: number}}=} functionInfo - + * if the functionInfo is already known, it can be passed in to avoid + * figuring it out again. + * @return {jQuery.Promise} - The promise will not complete until the + * hint has completed. Returns null, if the function hint is already + * displayed or there is no function hint at the cursor. + * + */ + JSParameterHintsProvider.prototype._getParameterHint = function (pushExistingHint, hint, functionInfo) { + var result = $.Deferred(); + functionInfo = functionInfo || this.session.getFunctionInfo(); + if (!functionInfo.inFunctionCall) { + this.cleanHintState(); + return result.reject(null); + } + + if (this.hasFunctionCallPosChanged(functionInfo.functionCallPos)) { + + var pushHint = pushExistingHint && this.isHintDisplayed(); + if (pushHint) { + this.pushHintOnStack(); + this.preserveHintStack = true; + } + + this.cleanHintState(); + this.preserveHintStack = false; + } else if (this.isHintDisplayed()) { + return result.reject(null); + } + + this.hintState.functionCallPos = functionInfo.functionCallPos; + + var request = null; + if (!hint) { + request = ScopeManager.requestParameterHint(this.session, functionInfo.functionCallPos); + } else { + this.session.setFnType(hint); + request = $.Deferred(); + request.resolveWith(null, [hint]); + } + + var self = this; + request.done(function (fnType) { + var hints = self.session.getParameterHint(functionInfo.functionCallPos); + hints.functionCallPos = functionInfo.functionCallPos; + result.resolve(hints); + }).fail(function () { + self.hintState = {}; + result.reject(null); + }); + + return result; + }; + + JSParameterHintsProvider.prototype.hasParameterHints = function () { + var functionInfo = this.session.getFunctionInfo(); + + return functionInfo.inFunctionCall; + }; + + JSParameterHintsProvider.prototype.getParameterHints = function (explicit, onCursorActivity) { + var functionInfo = this.session.getFunctionInfo(), + result = null; + + if (!onCursorActivity) { + if (functionInfo.inFunctionCall) { + var token = this.session.getToken(); + + if ((token && token.string === "(") || explicit) { + return this._getParameterHint(); + } + } else { + this.cleanHintState(); + } + + return $.Deferred().reject(null); + } + + if (!functionInfo.inFunctionCall) { + this.cleanHintState(); + return $.Deferred().reject(null); + } + + // If in a different function hint, then dismiss the old one and + // display the new one if there is one on the stack + if (this.hasFunctionCallPosChanged(functionInfo.functionCallPos)) { + if (this.popHintFromStack()) { + var poppedFunctionCallPos = this.hintState.functionCallPos, + currentFunctionCallPos = this.functionInfo.functionCallPos; + + if (poppedFunctionCallPos.line === currentFunctionCallPos.line && + poppedFunctionCallPos.ch === currentFunctionCallPos.ch) { + this.preserveHintStack = true; + result = this._getParameterHint(OVERWRITE_EXISTING_HINT, + this.hintState.fnType, functionInfo); + this.preserveHintStack = false; + return result; + } + } else { + this.cleanHintState(); + } + } + + var hints = this.session.getParameterHint(functionInfo.functionCallPos); + hints.functionCallPos = functionInfo.functionCallPos; + return $.Deferred().resolve(hints); + }; + + exports.JSParameterHintsProvider = JSParameterHintsProvider; +}); diff --git a/src/extensions/default/JavaScriptCodeHints/keyboard.json b/src/extensions/default/JavaScriptCodeHints/keyboard.json deleted file mode 100644 index d4d4e5d1345..00000000000 --- a/src/extensions/default/JavaScriptCodeHints/keyboard.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "showParameterHint": [ - { - "key": "Ctrl-Shift-Space" - }, - { - "key": "Ctrl-Shift-Space", - "platform": "mac" - } - ] -} \ No newline at end of file diff --git a/src/extensions/default/JavaScriptCodeHints/main.js b/src/extensions/default/JavaScriptCodeHints/main.js index 66b42a022f3..586ec1004c7 100644 --- a/src/extensions/default/JavaScriptCodeHints/main.js +++ b/src/extensions/default/JavaScriptCodeHints/main.js @@ -26,22 +26,24 @@ define(function (require, exports, module) { var _ = brackets.getModule("thirdparty/lodash"); - var CodeHintManager = brackets.getModule("editor/CodeHintManager"), - EditorManager = brackets.getModule("editor/EditorManager"), - Commands = brackets.getModule("command/Commands"), - CommandManager = brackets.getModule("command/CommandManager"), - LanguageManager = brackets.getModule("language/LanguageManager"), - AppInit = brackets.getModule("utils/AppInit"), - ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), - StringMatch = brackets.getModule("utils/StringMatch"), - ProjectManager = brackets.getModule("project/ProjectManager"), - PreferencesManager = brackets.getModule("preferences/PreferencesManager"), - Strings = brackets.getModule("strings"), - ParameterHintManager = require("ParameterHintManager"), - HintUtils = brackets.getModule("JSUtils/HintUtils"), - ScopeManager = brackets.getModule("JSUtils/ScopeManager"), - Session = brackets.getModule("JSUtils/Session"), - Acorn = require("node_modules/acorn/dist/acorn"); + var CodeHintManager = brackets.getModule("editor/CodeHintManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + Commands = brackets.getModule("command/Commands"), + CommandManager = brackets.getModule("command/CommandManager"), + LanguageManager = brackets.getModule("language/LanguageManager"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + StringMatch = brackets.getModule("utils/StringMatch"), + ProjectManager = brackets.getModule("project/ProjectManager"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), + JSParameterHintsProvider = require("./ParameterHintsProvider").JSParameterHintsProvider, + ParameterHintsManager = brackets.getModule("features/ParameterHintsManager"), + HintUtils = brackets.getModule("JSUtils/HintUtils"), + ScopeManager = brackets.getModule("JSUtils/ScopeManager"), + Session = brackets.getModule("JSUtils/Session"), + JumpToDefManager = brackets.getModule("features/JumpToDefManager"), + Acorn = require("node_modules/acorn/dist/acorn"); var session = null, // object that encapsulates the current session state cachedCursor = null, // last cursor of the current hinting session @@ -55,7 +57,8 @@ define(function (require, exports, module) { ignoreChange; // can ignore next "change" event if true; // Languages that support inline JavaScript - var _inlineScriptLanguages = ["html", "php"]; + var _inlineScriptLanguages = ["html", "php"], + phProvider = new JSParameterHintsProvider(); // Define the detectedExclusions which are files that have been detected to cause Tern to run out of control. PreferencesManager.definePreference("jscodehints.detectedExclusions", "array", [], { @@ -642,7 +645,7 @@ define(function (require, exports, module) { session = new Session(editor); ScopeManager.handleEditorChange(session, editor.document, previousEditor ? previousEditor.document : null); - ParameterHintManager.setSession(session); + phProvider.setSession(session); cachedHints = null; } @@ -667,11 +670,9 @@ define(function (require, exports, module) { .on(HintUtils.eventName("change"), function (event, editor, changeList) { if (!ignoreChange) { ScopeManager.handleFileChange(changeList); - ParameterHintManager.popUpHintAtOpenParen(); } ignoreChange = false; }); - ParameterHintManager.installListeners(editor); } else { session = null; } @@ -686,7 +687,6 @@ define(function (require, exports, module) { function uninstallEditorListeners(editor) { if (editor) { editor.off(HintUtils.eventName("change")); - ParameterHintManager.uninstallListeners(editor); } } @@ -719,10 +719,21 @@ define(function (require, exports, module) { installEditorListeners(current, previous); } - /* - * Handle JumptoDefiniton menu/keyboard command. + function setJumpPosition(curPos) { + EditorManager.getCurrentFullEditor().setCursorPos(curPos.line, curPos.ch, true); + } + + function JSJumpToDefProvider() { + } + + JSJumpToDefProvider.prototype.canJumpToDef = function (editor, implicitChar) { + return true; + }; + + /** + * Method to handle jump to definition feature. */ - function handleJumpToDefinition() { + JSJumpToDefProvider.prototype.doJumpToDef = function () { var offset, handleJumpResponse; @@ -856,7 +867,7 @@ define(function (require, exports, module) { requestJumpToDef(session, offset); return result.promise(); - } + }; /* * Helper for QuickEdit jump-to-definition request. @@ -891,18 +902,18 @@ define(function (require, exports, module) { // immediately install the current editor installEditorListeners(EditorManager.getActiveEditor()); + ParameterHintsManager.registerHintProvider(phProvider, ["javascript"], 0); // init - EditorManager.registerJumpToDefProvider(handleJumpToDefinition); + var jdProvider = new JSJumpToDefProvider(); + JumpToDefManager.registerJumpToDefProvider(jdProvider, ["javascript"], 0); var jsHints = new JSHints(); CodeHintManager.registerHintProvider(jsHints, HintUtils.SUPPORTED_LANGUAGES, 0); - ParameterHintManager.addCommands(); - // for unit testing exports.getSession = getSession; exports.jsHintProvider = jsHints; exports.initializeSession = initializeSession; - exports.handleJumpToDefinition = handleJumpToDefinition; + exports.handleJumpToDefinition = jdProvider.doJumpToDef.bind(jdProvider); }); }); diff --git a/src/extensions/default/JavaScriptCodeHints/unittests.js b/src/extensions/default/JavaScriptCodeHints/unittests.js index e8634d07219..7e31ebb28d2 100644 --- a/src/extensions/default/JavaScriptCodeHints/unittests.js +++ b/src/extensions/default/JavaScriptCodeHints/unittests.js @@ -22,7 +22,7 @@ */ /*jslint regexp: true */ -/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, beforeFirst, afterLast */ +/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, waitsForFail, beforeFirst, afterLast */ define(function (require, exports, module) { "use strict"; @@ -41,7 +41,8 @@ define(function (require, exports, module) { ScopeManager = brackets.getModule("JSUtils/ScopeManager"), HintUtils = brackets.getModule("JSUtils/HintUtils"), HintUtils2 = require("HintUtils2"), - ParameterHintManager = require("ParameterHintManager"); + ParameterHintProvider = require("ParameterHintsProvider").JSParameterHintsProvider, + phProvider = new ParameterHintProvider(); var extensionPath = FileUtils.getNativeModuleDirectoryPath(module), testPath = extensionPath + "/unittest-files/basic-test-files/file1.js", @@ -341,39 +342,26 @@ define(function (require, exports, module) { * Verify there is no parameter hint at the current cursor. */ function expectNoParameterHint() { - expect(ParameterHintManager.popUpHint()).toBe(null); + var requestStatus = undefined; + runs(function () { + var request = phProvider._getParameterHint(); + request.fail(function (status) { + requestStatus = status; + }); + + waitsForFail(request, "ParameterHints"); + }); + + runs(function () { + expect(requestStatus).toBe(null); + }); } /** * Verify the parameter hint is not visible. */ function expectParameterHintClosed() { - expect(ParameterHintManager.isHintDisplayed()).toBe(false); - } - - /* - * Wait for a hint response object to resolve, then apply a callback - * to the result - * - * @param {Object + jQuery.Deferred} hintObj - a hint response object, - * possibly deferred - * @param {Function} callback - the callback to apply to the resolved - * hint response object - */ - function _waitForParameterHint(hintObj, callback) { - var complete = false, - hint = null; - - hintObj.done(function () { - hint = JSCodeHints.getSession().getParameterHint(); - complete = true; - }); - - waitsFor(function () { - return complete; - }, "Expected parameter hint did not resolve", 3000); - - runs(function () { callback(hint); }); + expect(phProvider.isHintDisplayed()).toBe(false); } /** @@ -386,12 +374,9 @@ define(function (require, exports, module) { * @param {number} expectedParameter - the parameter at cursor. */ function expectParameterHint(expectedParams, expectedParameter) { - var request = ParameterHintManager.popUpHint(); - if (expectedParams === null) { - expect(request).toBe(null); - return; - } - + var requestHints = undefined, + request = null; + function expectHint(hint) { var params = hint.parameters, n = params.length, @@ -413,11 +398,29 @@ define(function (require, exports, module) { } } + + runs(function () { + request = phProvider._getParameterHint(); + + if (expectedParams === null) { + request.fail(function (result) { + requestHints = result; + }); + + waitsForFail(request, "ParameterHints"); + } else { + request.done(function (result) { + requestHints = result; + }); + + waitsForDone(request, "ParameterHints"); + } + }); - if (request) { - _waitForParameterHint(request, expectHint); + if (expectedParams === null) { + expect(requestHints).toBe(null); } else { - expectHint(JSCodeHints.getSession().getParameterHint()); + expectHint(requestHints); } } diff --git a/src/extensions/default/JavaScriptQuickEdit/main.js b/src/extensions/default/JavaScriptQuickEdit/main.js index 72d95ad8aea..2f1089dba48 100644 --- a/src/extensions/default/JavaScriptQuickEdit/main.js +++ b/src/extensions/default/JavaScriptQuickEdit/main.js @@ -31,7 +31,8 @@ define(function (require, exports, module) { LanguageManager = brackets.getModule("language/LanguageManager"), PerfUtils = brackets.getModule("utils/PerfUtils"), ProjectManager = brackets.getModule("project/ProjectManager"), - Strings = brackets.getModule("strings"); + Strings = brackets.getModule("strings"), + HealthLogger = brackets.getModule("utils/HealthLogger"); /** * Return the token string that is at the specified position. @@ -196,6 +197,13 @@ define(function (require, exports, module) { return null; } + //Send analytics data for Quick Edit open + HealthLogger.sendAnalyticsData( + "QuickEditOpen", + "usage", + "quickEdit", + "open" + ); // Only provide JavaScript editor if the selection is within a single line var sel = hostEditor.getSelection(); if (sel.start.line !== sel.end.line) { diff --git a/src/extensions/default/JavaScriptQuickEdit/unittests.js b/src/extensions/default/JavaScriptQuickEdit/unittests.js index 5302abca973..a452d5b0fb2 100644 --- a/src/extensions/default/JavaScriptQuickEdit/unittests.js +++ b/src/extensions/default/JavaScriptQuickEdit/unittests.js @@ -21,7 +21,7 @@ * */ -/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone */ +/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, waitsForFail */ define(function (require, exports, module) { "use strict"; @@ -275,7 +275,7 @@ define(function (require, exports, module) { describe("Code hints tests within quick edit window ", function () { var JSCodeHints, - ParameterHintManager; + ParameterHintProvider; /* * Ask provider for hints at current cursor position; expect it to @@ -345,31 +345,6 @@ define(function (require, exports, module) { }); } - /* - * Wait for a hint response object to resolve, then apply a callback - * to the result - * - * @param {Object + jQuery.Deferred} hintObj - a hint response object, - * possibly deferred - * @param {Function} callback - the callback to apply to the resolved - * hint response object - */ - function _waitForParameterHint(hintObj, callback) { - var complete = false, - hint = null; - - hintObj.done(function () { - hint = JSCodeHints.getSession().getParameterHint(); - complete = true; - }); - - waitsFor(function () { - return complete; - }, "Expected parameter hint did not resolve", 3000); - - runs(function () { callback(hint); }); - } - /** * Show a function hint based on the code at the cursor. Verify the * hint matches the passed in value. @@ -380,11 +355,8 @@ define(function (require, exports, module) { * @param {number} expectedParameter - the parameter at cursor. */ function expectParameterHint(expectedParams, expectedParameter) { - var request = ParameterHintManager.popUpHint(); - if (expectedParams === null) { - expect(request).toBe(null); - return; - } + var requestHints = undefined, + request = null; function expectHint(hint) { var params = hint.parameters, @@ -408,10 +380,28 @@ define(function (require, exports, module) { } - if (request) { - _waitForParameterHint(request, expectHint); + runs(function () { + request = ParameterHintProvider._getParameterHint(); + + if (expectedParams === null) { + request.fail(function (result) { + requestHints = result; + }); + + waitsForFail(request, "ParameterHints"); + } else { + request.done(function (result) { + requestHints = result; + }); + + waitsForDone(request, "ParameterHints"); + } + }); + + if (expectedParams === null) { + expect(requestHints).toBe(null); } else { - expectHint(JSCodeHints.getSession().getParameterHint()); + expectHint(requestHints); } } @@ -462,7 +452,7 @@ define(function (require, exports, module) { var extensionRequire = testWindow.brackets.getModule("utils/ExtensionLoader"). getRequireContextForExtension("JavaScriptCodeHints"); JSCodeHints = extensionRequire("main"); - ParameterHintManager = extensionRequire("ParameterHintManager"); + ParameterHintProvider = extensionRequire("ParameterHintsProvider").JSParameterHintsProvider(); } beforeEach(function () { @@ -472,7 +462,7 @@ define(function (require, exports, module) { afterEach(function () { JSCodeHints = null; - ParameterHintManager = null; + ParameterHintProvider = null; }); it("should see code hint lists in quick editor", function () { diff --git a/src/extensions/default/JavaScriptRefactoring/ExtractToVariable.js b/src/extensions/default/JavaScriptRefactoring/ExtractToVariable.js index 554041900aa..e5cb1b70412 100644 --- a/src/extensions/default/JavaScriptRefactoring/ExtractToVariable.js +++ b/src/extensions/default/JavaScriptRefactoring/ExtractToVariable.js @@ -37,19 +37,24 @@ define(function(require, exports, module) { * Does the actual extraction. i.e Replacing the text, Creating a variable * and multi select variable names */ - function extract(scopes, parentStatement, expns, text) { + function extract(scopes, parentStatement, expns, text, insertPosition) { var varType = "var", varName = RefactoringUtils.getUniqueIdentifierName(scopes, "extracted"), varDeclaration = varType + " " + varName + " = " + text + ";\n", - insertStartPos = session.editor.posFromIndex(parentStatement.start), + parentStatementStartPos = session.editor.posFromIndex(parentStatement.start), + insertStartPos = insertPosition || parentStatementStartPos, selections = [], doc = session.editor.document, replaceExpnIndex = 0, - posToIndent; + posToIndent, + edits = []; // If parent statement is expression statement, then just append var declaration // Ex: "add(1, 2)" will become "var extracted = add(1, 2)" - if (parentStatement.type === "ExpressionStatement" && RefactoringUtils.isEqual(parentStatement.expression, expns[0])) { + if (parentStatement.type === "ExpressionStatement" && + RefactoringUtils.isEqual(parentStatement.expression, expns[0]) && + insertStartPos.line === parentStatementStartPos.line && + insertStartPos.ch === parentStatementStartPos.ch) { varDeclaration = varType + " " + varName + " = "; replaceExpnIndex = 1; } @@ -63,9 +68,16 @@ define(function(require, exports, module) { expns[i].start = doc.adjustPosForChange(expns[i].start, varDeclaration.split("\n"), insertStartPos, insertStartPos); expns[i].end = doc.adjustPosForChange(expns[i].end, varDeclaration.split("\n"), insertStartPos, insertStartPos); - selections.push({ - start: expns[i].start, - end: {line: expns[i].start.line, ch: expns[i].start.ch + varName.length} + edits.push({ + edit: { + text: varName, + start: expns[i].start, + end: expns[i].end + }, + selection: { + start: expns[i].start, + end: {line: expns[i].start.line, ch: expns[i].start.ch + varName.length} + } }); } @@ -73,15 +85,12 @@ define(function(require, exports, module) { doc.batchOperation(function() { doc.replaceRange(varDeclaration, insertStartPos); - for (var i = replaceExpnIndex; i < expns.length; ++i) { - doc.replaceRange(varName, expns[i].start, expns[i].end); - } + selections = doc.doMultipleEdits(edits); selections.push({ start: {line: insertStartPos.line, ch: insertStartPos.ch + varType.length + 1}, end: {line: insertStartPos.line, ch: insertStartPos.ch + varType.length + varName.length + 1}, primary: true }); - session.editor.setSelections(selections); session.editor._codeMirror.indentLine(posToIndent.line, "smart"); }); @@ -163,6 +172,7 @@ define(function(require, exports, module) { */ function extractToVariable(ast, start, end, text, scopes) { var doc = session.editor.document, + editor = EditorManager.getActiveEditor(), parentExpn = RefactoringUtils.getExpression(ast, start, end, doc.getText()), expns = [], parentBlockStatement, @@ -178,8 +188,26 @@ define(function(require, exports, module) { if (doc.getText().substr(parentExpn.start, parentExpn.end - parentExpn.start) === text) { parentBlockStatement = RefactoringUtils.findSurroundASTNode(ast, parentExpn, ["BlockStatement", "Program"]); expns = findAllExpressions(parentBlockStatement, parentExpn, text); - parentStatement = RefactoringUtils.findSurroundASTNode(ast, expns[0], ["Statement"]); - extract(scopes, parentStatement, expns, text); + + RefactoringUtils.getScopeData(session, editor.posFromIndex(expns[0].start)).done(function(scope) { + var firstExpnsScopes = RefactoringUtils.getAllScopes(ast, scope, doc.getText()), + insertPostion; + parentStatement = RefactoringUtils.findSurroundASTNode(ast, expns[0], ["Statement"]); + if (scopes.length < firstExpnsScopes.length) { + var parentScope; + if (expns[0].body && expns[0].body.type === "BlockStatement") { + parentScope = firstExpnsScopes[firstExpnsScopes.length - scopes.length]; + } else { + parentScope = firstExpnsScopes[firstExpnsScopes.length - scopes.length - 1]; + } + + var insertNode = RefactoringUtils.findSurroundASTNode(ast, parentScope.originNode, ["Statement"]); + if (insertNode) { + insertPostion = session.editor.posFromIndex(insertNode.start); + } + } + extract(scopes, parentStatement, expns, text, insertPostion); + }); } else { parentStatement = RefactoringUtils.findSurroundASTNode(ast, parentExpn, ["Statement"]); extract(scopes, parentStatement, [{ start: start, end: end }], text); @@ -212,6 +240,16 @@ define(function(require, exports, module) { expns, inlineMenu; + function callExtractToVariable(startPos, endPos, value) { + RefactoringUtils.getScopeData(session, editor.posFromIndex(startPos)) + .done(function(expnscope) { + scopes = RefactoringUtils.getAllScopes(ast, expnscope, doc.getText()); + extractToVariable(ast, startPos, endPos, value, scopes); + }).fail(function() { + editor.displayErrorMessageAtCursor(Strings.ERROR_TERN_FAILED); + }); + } + RefactoringUtils.getScopeData(session, editor.posFromIndex(start)).done(function(scope) { ast = RefactoringUtils.getAST(doc.getText()); scopes = RefactoringUtils.getAllScopes(ast, scope, doc.getText()); @@ -253,7 +291,7 @@ define(function(require, exports, module) { // If only one surround expression, extract if (expns.length === 1) { - extractToVariable(ast, expns[0].start, expns[0].end, expns[0].value, scopes); + callExtractToVariable(expns[0].start, expns[0].end, expns[0].value); return; } @@ -265,13 +303,26 @@ define(function(require, exports, module) { inlineMenu = new InlineMenu(session.editor, Strings.EXTRACTTO_VARIABLE_SELECT_EXPRESSION); inlineMenu.onHover(function (expnId) { + // Remove the scroll Handlers If already Attached. + editor.off("scroll.inlinemenu"); + // Add a scroll handler If Selection Range is not View. + // This is Added for a Bug, where Menu used not to open for the first Time + if(!editor.isLineVisible(editor.posFromIndex(expns[expnId].end).line)) { + editor.on("scroll.inlinemenu", function() { + // Remove the Handlers so that If scroll event is triggerd again by any other operation + // Menu should not be reopened. + // Menu Should be reopened only if Scroll event is triggered by onHover. + editor.off("scroll.inlinemenu"); + inlineMenu.openRemovedMenu(); + }); + } editor.setSelection(editor.posFromIndex(expns[expnId].start), editor.posFromIndex(expns[expnId].end)); }); inlineMenu.open(expns); inlineMenu.onSelect(function (expnId) { - extractToVariable(ast, expns[expnId].start, expns[expnId].end, expns[expnId].value, scopes); + callExtractToVariable(expns[expnId].start, expns[expnId].end, expns[expnId].value); inlineMenu.close(); }); diff --git a/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js b/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js index 76bc5b91b3a..c92e1f6f00a 100644 --- a/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js +++ b/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js @@ -467,7 +467,14 @@ define(function (require, exports, module) { * @return {Object} - Ast of current opened doc */ RefactoringSession.prototype.createAstOfCurrentDoc = function () { - return AcornLoose.parse_dammit(this.document.getText()); + var ast, + text = this.document.getText(); + try { + ast = Acorn.parse(text); + } catch(e) { + ast = Acorn.parse_dammit(text); + } + return ast; }; /** diff --git a/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js b/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js index 80e0d279e12..6fa53b5f48e 100644 --- a/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js +++ b/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js @@ -29,7 +29,8 @@ define(function (require, exports, module) { Session = brackets.getModule("JSUtils/Session"), MessageIds = brackets.getModule("JSUtils/MessageIds"), TokenUtils = brackets.getModule("utils/TokenUtils"), - Strings = brackets.getModule("strings"); + Strings = brackets.getModule("strings"), + ProjectManager = brackets.getModule("project/ProjectManager"); var session = null, // object that encapsulates the current session state keywords = ["define", "alert", "exports", "require", "module", "arguments"]; @@ -97,8 +98,22 @@ define(function (require, exports, module) { var result = new $.Deferred(); function isInSameFile(obj, refsResp) { + var projectRoot = ProjectManager.getProjectRoot(), + projectDir, + fileName = ""; + if (projectRoot) { + projectDir = projectRoot.fullPath; + } + + // get the relative path of File as Tern can also return + // references with file name as a relative path wrt projectRoot + // so refernce file name will be compared with both relative and absolute path to check if it is same file + if (projectDir && refsResp && refsResp.file && refsResp.file.indexOf(projectDir) === 0) { + fileName = refsResp.file.slice(projectDir.length); + } // In case of unsaved files, After renameing once Tern is returning filename without forward slash - return (obj && (obj.file === refsResp.file || obj.file === refsResp.file.slice(1, refsResp.file.length))); + return (obj && (obj.file === refsResp.file || obj.file === fileName + || obj.file === refsResp.file.slice(1, refsResp.file.length))); } /** @@ -127,13 +142,23 @@ define(function (require, exports, module) { } } - if (type === "local") { - editor.setSelections(refs); - } else { - editor.setSelections(refs.filter(function(element) { + var currentPosition = editor.posFromIndex(refsResp.offset), + refsArray = refs; + if (type !== "local") { + refsArray = refs.filter(function (element) { return isInSameFile(element, refsResp); - })); + }); } + + // Finding the Primary Reference in Array + var primaryRef = refsArray.find(function (element) { + return ((element.start.line === currentPosition.line || element.end.line === currentPosition.line) + && currentPosition.ch <= element.end.ch && currentPosition.ch >= element.start.ch); + }); + // Setting the primary flag of Primary Refence to true + primaryRef.primary = true; + + editor.setSelections(refsArray); } /** diff --git a/src/extensions/default/JavaScriptRefactoring/WrapSelection.js b/src/extensions/default/JavaScriptRefactoring/WrapSelection.js index 6d6dad468a2..7964b2a4cc9 100644 --- a/src/extensions/default/JavaScriptRefactoring/WrapSelection.js +++ b/src/extensions/default/JavaScriptRefactoring/WrapSelection.js @@ -97,7 +97,7 @@ define(function (require, exports, module) { }); if (wrapperName === TRY_CATCH) { - var cursorLine = current.editor.getSelection().start.line - 1, + var cursorLine = current.editor.getSelection().end.line - 1, startCursorCh = current.document.getLine(cursorLine).indexOf("\/\/"), endCursorCh = current.document.getLine(cursorLine).length; @@ -246,9 +246,11 @@ define(function (require, exports, module) { } var token = TokenUtils.getTokenAt(current.cm, current.cm.posFromIndex(endIndex)), + commaString = ",", isLastNode, - lineEndPos, - templateParams; + templateParams, + parentNode, + propertyEndPos; //Create getters and setters only if selected reference is a property if (token.type !== "property") { @@ -256,15 +258,48 @@ define(function (require, exports, module) { return; } + parentNode = current.getParentNode(current.ast, endIndex); // Check if selected propery is child of a object expression - if (!current.getParentNode(current.ast, endIndex)) { + if (!parentNode || !parentNode.properties) { current.editor.displayErrorMessageAtCursor(Strings.ERROR_GETTERS_SETTERS); return; } + + var propertyNodeArray = parentNode.properties; + // Find the last Propery Node before endIndex + var properyNodeIndex = propertyNodeArray.findIndex(function (element) { + return (endIndex >= element.start && endIndex < element.end); + }); + + var propertyNode = propertyNodeArray[properyNodeIndex]; + + //Get Current Selected Property End Index; + propertyEndPos = editor.posFromIndex(propertyNode.end); + + //We have to add ',' so we need to find position of current property selected isLastNode = current.isLastNodeInScope(current.ast, endIndex); - lineEndPos = current.lineEndPosition(current.startPos.line); + var nextPropertNode, nextPropertyStartPos; + if(!isLastNode && properyNodeIndex + 1 <= propertyNodeArray.length - 1) { + nextPropertNode = propertyNodeArray[properyNodeIndex + 1]; + nextPropertyStartPos = editor.posFromIndex(nextPropertNode.start); + + if(propertyEndPos.line !== nextPropertyStartPos.line) { + propertyEndPos = current.lineEndPosition(current.startPos.line); + } else { + propertyEndPos = nextPropertyStartPos; + commaString = ", "; + } + } + + var getSetPos; + if (isLastNode) { + getSetPos = current.document.adjustPosForChange(propertyEndPos, commaString.split("\n"), + propertyEndPos, propertyEndPos); + } else { + getSetPos = propertyEndPos; + } templateParams = { "getName": token.string, "setName": token.string, @@ -276,18 +311,17 @@ define(function (require, exports, module) { current.document.batchOperation(function() { if (isLastNode) { //Add ',' in the end of current line - current.document.replaceRange(",", lineEndPos, lineEndPos); - lineEndPos.ch++; + current.document.replaceRange(commaString, propertyEndPos, propertyEndPos); } - current.editor.setSelection(lineEndPos); //Selection on line end + current.editor.setSelection(getSetPos); //Selection on line end // Add getters and setters for given token using template at current cursor position current.replaceTextFromTemplate(GETTERS_SETTERS, templateParams); if (!isLastNode) { // Add ',' at the end setter - current.document.replaceRange(",", current.editor.getSelection().start, current.editor.getSelection().start); + current.document.replaceRange(commaString, current.editor.getSelection().start, current.editor.getSelection().start); } }); } diff --git a/src/extensions/default/JavaScriptRefactoring/main.js b/src/extensions/default/JavaScriptRefactoring/main.js index 4bc59753072..429256fa2a4 100644 --- a/src/extensions/default/JavaScriptRefactoring/main.js +++ b/src/extensions/default/JavaScriptRefactoring/main.js @@ -33,7 +33,10 @@ define(function (require, exports, module) { ExtractToFunction = require("ExtractToFunction"), WrapSelection = require("WrapSelection"), CommandManager = brackets.getModule("command/CommandManager"), - Menus = brackets.getModule("command/Menus"); + Menus = brackets.getModule("command/Menus"), + HealthLogger = brackets.getModule("utils/HealthLogger"), + _ = brackets.getModule("thirdparty/lodash"), + EditorManager = brackets.getModule("editor/EditorManager"); var jsRefactoringEnabled = true; @@ -66,6 +69,63 @@ define(function (require, exports, module) { jsRefactoringEnabled = _isRefactoringEnabled(); }); + function _handleRefactor(functionName) { + var eventName, eventType = ""; + + switch (functionName) { + case REFACTOR_RENAME: + eventName = REFACTOR_RENAME; + eventType = "rename"; + RenameIdentifier.handleRename(); + break; + case EXTRACTTO_VARIABLE: + eventName = EXTRACTTO_VARIABLE; + eventType = "extractToVariable"; + ExtractToVariable.handleExtractToVariable(); + break; + case EXTRACTTO_FUNCTION: + eventName = EXTRACTTO_FUNCTION; + eventType = "extractToFunction"; + ExtractToFunction.handleExtractToFunction(); + break; + case REFACTORWRAPINTRYCATCH: + eventName = REFACTORWRAPINTRYCATCH; + eventType = "tryCatch"; + WrapSelection.wrapInTryCatch(); + break; + case REFACTORWRAPINCONDITION: + eventName = REFACTORWRAPINCONDITION; + eventType = "wrapInCondition"; + WrapSelection.wrapInCondition(); + break; + case REFACTORCONVERTTOARROWFN: + eventName = REFACTORCONVERTTOARROWFN; + eventType = "convertToFunction"; + WrapSelection.convertToArrowFunction(); + break; + case REFACTORCREATEGETSET: + eventName = REFACTORCREATEGETSET; + eventType = "createGetterSetter"; + WrapSelection.createGettersAndSetters(); + break; + } + if (eventName) { + var editor = EditorManager.getActiveEditor(); + + // Logging should be done only when the context is javascript + if (!editor || editor.getModeForSelection() !== "javascript") { + return; + } + // Send analytics data for js refactoring + HealthLogger.sendAnalyticsData( + eventName, + "usage", + "jsRefactor", + eventType + ); + } + } + AppInit.appReady(function () { if (jsRefactoringEnabled) { @@ -76,34 +136,34 @@ define(function (require, exports, module) { Menus.getMenu(menuLocation).addMenuDivider(); // Rename Identifier - CommandManager.register(Strings.CMD_REFACTORING_RENAME, REFACTOR_RENAME, RenameIdentifier.handleRename); + CommandManager.register(Strings.CMD_REFACTORING_RENAME, REFACTOR_RENAME, _.partial(_handleRefactor, REFACTOR_RENAME)); subMenu.addMenuItem(REFACTOR_RENAME); Menus.getMenu(menuLocation).addMenuItem(REFACTOR_RENAME, KeyboardPrefs.renameIdentifier); // Extract to Variable - CommandManager.register(Strings.CMD_EXTRACTTO_VARIABLE, EXTRACTTO_VARIABLE, ExtractToVariable.handleExtractToVariable); + CommandManager.register(Strings.CMD_EXTRACTTO_VARIABLE, EXTRACTTO_VARIABLE, _.partial(_handleRefactor, EXTRACTTO_VARIABLE)); subMenu.addMenuItem(EXTRACTTO_VARIABLE); Menus.getMenu(menuLocation).addMenuItem(EXTRACTTO_VARIABLE, KeyboardPrefs.extractToVariable); // Extract to Function - CommandManager.register(Strings.CMD_EXTRACTTO_FUNCTION, EXTRACTTO_FUNCTION, ExtractToFunction.handleExtractToFunction); + CommandManager.register(Strings.CMD_EXTRACTTO_FUNCTION, EXTRACTTO_FUNCTION, _.partial(_handleRefactor, EXTRACTTO_FUNCTION)); subMenu.addMenuItem(EXTRACTTO_FUNCTION); Menus.getMenu(menuLocation).addMenuItem(EXTRACTTO_FUNCTION, KeyboardPrefs.extractToFunction); // Wrap Selection - CommandManager.register(Strings.CMD_REFACTORING_TRY_CATCH, REFACTORWRAPINTRYCATCH, WrapSelection.wrapInTryCatch); + CommandManager.register(Strings.CMD_REFACTORING_TRY_CATCH, REFACTORWRAPINTRYCATCH, _.partial(_handleRefactor, REFACTORWRAPINTRYCATCH)); subMenu.addMenuItem(REFACTORWRAPINTRYCATCH); Menus.getMenu(menuLocation).addMenuItem(REFACTORWRAPINTRYCATCH); - CommandManager.register(Strings.CMD_REFACTORING_CONDITION, REFACTORWRAPINCONDITION, WrapSelection.wrapInCondition); + CommandManager.register(Strings.CMD_REFACTORING_CONDITION, REFACTORWRAPINCONDITION, _.partial(_handleRefactor, REFACTORWRAPINCONDITION)); subMenu.addMenuItem(REFACTORWRAPINCONDITION); Menus.getMenu(menuLocation).addMenuItem(REFACTORWRAPINCONDITION); - CommandManager.register(Strings.CMD_REFACTORING_ARROW_FUNCTION, REFACTORCONVERTTOARROWFN, WrapSelection.convertToArrowFunction); + CommandManager.register(Strings.CMD_REFACTORING_ARROW_FUNCTION, REFACTORCONVERTTOARROWFN, _.partial(_handleRefactor, REFACTORCONVERTTOARROWFN)); subMenu.addMenuItem(REFACTORCONVERTTOARROWFN); Menus.getMenu(menuLocation).addMenuItem(REFACTORCONVERTTOARROWFN); - CommandManager.register(Strings.CMD_REFACTORING_GETTERS_SETTERS, REFACTORCREATEGETSET, WrapSelection.createGettersAndSetters); + CommandManager.register(Strings.CMD_REFACTORING_GETTERS_SETTERS, REFACTORCREATEGETSET, _.partial(_handleRefactor, REFACTORCREATEGETSET)); subMenu.addMenuItem(REFACTORCREATEGETSET); Menus.getMenu(menuLocation).addMenuItem(REFACTORCREATEGETSET); } diff --git a/src/extensions/default/MDNDocs/InlineDocsViewer.js b/src/extensions/default/MDNDocs/InlineDocsViewer.js index 25372e10507..d3ab81abd9c 100644 --- a/src/extensions/default/MDNDocs/InlineDocsViewer.js +++ b/src/extensions/default/MDNDocs/InlineDocsViewer.js @@ -33,7 +33,8 @@ define(function (require, exports, module) { InlineWidget = brackets.getModule("editor/InlineWidget").InlineWidget, KeyEvent = brackets.getModule("utils/KeyEvent"), Strings = brackets.getModule("strings"), - Mustache = brackets.getModule("thirdparty/mustache/mustache"); + Mustache = brackets.getModule("thirdparty/mustache/mustache"), + HealthLogger = brackets.getModule("utils/HealthLogger"); // Load template var inlineEditorTemplate = require("text!InlineDocsViewer.html"); @@ -73,6 +74,8 @@ define(function (require, exports, module) { this.$scroller = this.$wrapperDiv.find(".scroller"); this.$scroller.on("mousewheel", this._handleWheelScroll); + this.$moreinfo = this.$wrapperDiv.find(".more-info"); + this.$moreinfo.on("click", this._logAnalyticsData); this._onKeydown = this._onKeydown.bind(this); } @@ -191,6 +194,21 @@ define(function (require, exports, module) { InlineDocsViewer.prototype._sizeEditorToContent = function () { this.hostEditor.setInlineWidgetHeight(this, this.$wrapperDiv.height() + 20, true); }; + + /** + * Send analytics data for Quick Doc "readMore" action + * + * @return {boolean} false + */ + InlineDocsViewer.prototype._logAnalyticsData = function () { + HealthLogger.sendAnalyticsData( + "QuickDocReadMore", + "usage", + "quickDoc", + "readMore" + ); + return false; + }; module.exports = InlineDocsViewer; diff --git a/src/extensions/default/MDNDocs/main.js b/src/extensions/default/MDNDocs/main.js index 356347918c2..31d123176a1 100644 --- a/src/extensions/default/MDNDocs/main.js +++ b/src/extensions/default/MDNDocs/main.js @@ -31,7 +31,8 @@ define(function (require, exports, module) { FileUtils = brackets.getModule("file/FileUtils"), CSSUtils = brackets.getModule("language/CSSUtils"), HTMLUtils = brackets.getModule("language/HTMLUtils"), - ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + HealthLogger = brackets.getModule("utils/HealthLogger"); // Extension modules var InlineDocsViewer = require("InlineDocsViewer"); @@ -102,6 +103,14 @@ define(function (require, exports, module) { return null; } + // Send analytics data for Quick Doc open + HealthLogger.sendAnalyticsData( + "cssQuickDoc", + "usage", + "quickDoc", + "open" + ); + // Only provide docs if the selection is within a single line var sel = hostEditor.getSelection(); if (sel.start.line !== sel.end.line) { diff --git a/src/extensions/default/OpenWithExternalApplication/GraphicsFile.js b/src/extensions/default/OpenWithExternalApplication/GraphicsFile.js new file mode 100644 index 00000000000..1ffc72c1173 --- /dev/null +++ b/src/extensions/default/OpenWithExternalApplication/GraphicsFile.js @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2013 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + + var PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), + StringsUtils = brackets.getModule("utils/StringUtils"), + ProjectManager = brackets.getModule("project/ProjectManager"), + Dialogs = brackets.getModule("widgets/Dialogs"), + DefaultDialogs = brackets.getModule("widgets/DefaultDialogs"), + HealthLogger = brackets.getModule("utils/HealthLogger"); + + + var _requestID = 0, + _initialized = false; + + var _graphicsFileTypes = ["jpg", "jpeg", "png", "svg", "xd", "psd", "ai"]; + + var _nodeDomain; + + function init(nodeDomain) { + + if (_initialized) { + return; + } + _initialized = true; + + _nodeDomain = nodeDomain; + + _nodeDomain.on('checkFileTypesInFolderResponse', function (event, response) { + if (response.id !== _requestID) { + return; + } + _graphicsFilePresentInProject(response.present); + }); + + ProjectManager.on("projectOpen", function () { + _checkForGraphicsFileInPrjct(); + }); + + _checkForGraphicsFileInPrjct(); + + } + + + function _checkForGraphicsFileInPrjct() { + + if (PreferencesManager.getViewState("AssociateGraphicsFileDialogShown")) { + return; + } + + var userUuid = PreferencesManager.getViewState("UUID"), + olderUuid = PreferencesManager.getViewState("OlderUUID"); + + if(!(userUuid || olderUuid)) { + return; + } + + _nodeDomain.exec("checkFileTypesInFolder", { + extensions: _graphicsFileTypes.join(), + folder: ProjectManager.getProjectRoot().fullPath, + reqId: ++_requestID + }); + + } + + function _graphicsFilePresentInProject(isPresent) { + + if (!isPresent) { + return; + } + + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.ASSOCIATE_GRAPHICS_FILE_TO_DEFAULT_APP_TITLE, + Strings.ASSOCIATE_GRAPHICS_FILE_TO_DEFAULT_APP_MSG, + [ + { className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, + text: Strings.CANCEL + }, + { className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, + text: Strings.OK + } + ] + ).done(function (id) { + + if (id !== Dialogs.DIALOG_BTN_OK) { + HealthLogger.sendAnalyticsData( + "externalEditorsCancelled", + "usage", + "externalEditors", + "Cancelled", + "" + ); + return; + } + HealthLogger.sendAnalyticsData( + "LinkExternalEditors", + "usage", + "externalEditors", + "LinkExternalEditors", + "" + ); + + brackets.app.getSystemDefaultApp(_graphicsFileTypes.join(), function (err, out) { + + if (err) { + return; + } + var associateApp = out.split(','), + fileTypeToAppMap = {}, + AppToFileTypeMap = {}; + + associateApp.forEach(function (item) { + + var filetype = item.split('##')[0], + app = item.split('##')[1]; + + if (!filetype) { + return; + } + + if (filetype === "xd") { + if (app.toLowerCase() !== "adobe xd" && app.toLowerCase() !== "adobe.cc.xd") { + return; + } + + app = "Adobe XD"; + } + fileTypeToAppMap[filetype] = app; + + if (brackets.platform === "win" && app.toLowerCase().endsWith('.exe')) { + app = app.substring(app.lastIndexOf('\\') + 1, app.length - 4); + } + if (AppToFileTypeMap[app]) { + AppToFileTypeMap[app].push(filetype); + } else { + AppToFileTypeMap[app] = [filetype]; + } + }); + + var prefs = PreferencesManager.get('externalApplications'); + + for (var key in fileTypeToAppMap) { + if (fileTypeToAppMap.hasOwnProperty(key)) { + if(key && !prefs[key]) { + prefs[key] = fileTypeToAppMap[key]; + if(brackets.platform === "win" && !fileTypeToAppMap[key].toLowerCase().endsWith('.exe')) { + prefs[key] = "default"; + } + HealthLogger.sendAnalyticsData( + "AddExternalEditorForFileType_" + key.toUpperCase(), + "usage", + "externalEditors", + "AddExternalEditorForFileType_" + key.toUpperCase(), + "" + ); + } + } + } + + PreferencesManager.set('externalApplications', prefs); + + var str = ""; + for(var app in AppToFileTypeMap) { + str += AppToFileTypeMap[app].join() + "->" + app + "
"; + } + + if(!str) { + return; + } + + str+="
"; + + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.ASSOCIATE_GRAPHICS_FILE_TO_DEFAULT_APP_TITLE, + StringsUtils.format(Strings.ASSOCIATE_GRAPHICS_FILE_TO_DEFAULT_APP_CNF_MSG, str), + [ + { className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, + text: Strings.OK + } + ] + ); + }); + }); + PreferencesManager.setViewState("AssociateGraphicsFileDialogShown", true); + + } + + exports.init = init; + +}); diff --git a/src/extensions/default/OpenWithExternalApplication/main.js b/src/extensions/default/OpenWithExternalApplication/main.js new file mode 100644 index 00000000000..3589f994c49 --- /dev/null +++ b/src/extensions/default/OpenWithExternalApplication/main.js @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + + var AppInit = brackets.getModule("utils/AppInit"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), + FileViewController = brackets.getModule("project/FileViewController"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + NodeDomain = brackets.getModule("utils/NodeDomain"), + FileUtils = brackets.getModule("file/FileUtils"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + GraphicsFile = require("GraphicsFile"); + + /** + * @private + * @type {string} fullPath of the OpenWithExternalEditor Domain implementation + */ + var _domainPath = ExtensionUtils.getModulePath(module, "node/OpenWithExternalApplicationDomain"); + + /** + * @private + * @type {NodeDomain} + */ + var _nodeDomain = new NodeDomain("OpenWithExternalApplication", _domainPath); + + var extensionToExtApplicationMap = {}; + + function convertUnixPathToWindowsPath(path) { + if (brackets.platform === "win" && path && FileSystem.isAbsolutePath(path)) { + path = path.replace(RegExp('/','g'), '\\'); + } + return path; + } + + function _openWithExternalApplication(event, path) { + _nodeDomain.exec("open", { + path: convertUnixPathToWindowsPath(path), + app: extensionToExtApplicationMap[FileUtils.getFileExtension(path).toLowerCase()] + }); + } + + PreferencesManager.definePreference("externalApplications", "object", {}, { + description: Strings.DESCRIPTION_EXTERNAL_APPLICATION_ASSOCIATE + }); + + PreferencesManager.on("change", "externalApplications", function () { + extensionToExtApplicationMap = PreferencesManager.get("externalApplications"); + FileUtils.addExtensionToExternalAppList(Object.keys(extensionToExtApplicationMap)); + }); + + FileViewController.on("openWithExternalApplication", _openWithExternalApplication); + + AppInit.appReady(function () { + + GraphicsFile.init(_nodeDomain); + extensionToExtApplicationMap = PreferencesManager.get("externalApplications"); + FileUtils.addExtensionToExternalAppList(Object.keys(extensionToExtApplicationMap)); + }); +}); diff --git a/src/extensions/default/OpenWithExternalApplication/node/OpenWithExternalApplicationDomain.js b/src/extensions/default/OpenWithExternalApplication/node/OpenWithExternalApplicationDomain.js new file mode 100644 index 00000000000..e5a7e414585 --- /dev/null +++ b/src/extensions/default/OpenWithExternalApplication/node/OpenWithExternalApplicationDomain.js @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2012 - 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. + * + */ + +/*eslint-env node */ +/*jslint node: true */ +"use strict"; + +var open = require("open"), + Glob = require("glob").Glob, + path = require('path'); + +var _domainManager; + +/** + * @private + * + * @param {Object} params Object to use + */ +function _openWithExternalApplication(params) { + var application = "default" === params.app ? "": params.app; + open(params.path, application); +} + +/** + * @private + * + * @param {Object} params Object to use + */ +function _checkFileTypesInFolder(params) { + + var extList = params.extensions, + dirPath = path.normalize(params.folder), + + pattern = dirPath + "/**/*.+(" + extList.replace(/,/g, "|") + ")"; + + var globMgr = new Glob(pattern, function (err, matches) { + + var respObj = { + id: params.reqId, + present: matches.length > 0 ? true : false + }; + _domainManager.emitEvent('OpenWithExternalApplication', 'checkFileTypesInFolderResponse', [respObj]); + }); + + globMgr.on("match", function() { + globMgr.abort(); + var respObj = { + id: params.reqId, + present: true + }; + _domainManager.emitEvent('OpenWithExternalApplication', 'checkFileTypesInFolderResponse', [respObj]); + }); + +} + + +/** + * Initializes the OpenWithExternalEditor domain with its commands. + * @param {DomainManager} domainManager The DomainManager for the server + */ +function init(domainManager) { + _domainManager = domainManager; + + if (!domainManager.hasDomain("OpenWithExternalApplication")) { + domainManager.registerDomain("OpenWithExternalApplication", {major: 0, minor: 1}); + } + + _domainManager.registerCommand( + "OpenWithExternalApplication", + "open", + _openWithExternalApplication, + true, + "open document with External Application.", + [{ + name: "params", + type: "object", + description: "Params Object having document and App Path." + }], + [] + ); + + _domainManager.registerCommand( + "OpenWithExternalApplication", + "checkFileTypesInFolder", + _checkFileTypesInFolder, + true, + "looks for File Types in a folder.", + [{ + name: "params", + type: "object", + description: "Params Object having File Extensions and Folder Path." + }], + [] + ); + + _domainManager.registerEvent( + "OpenWithExternalApplication", + "checkFileTypesInFolderResponse", + [ + { + name: "msgObj", + type: "object", + description: "json object containing message info to pass to brackets" + } + ] + ); +} + +exports.init = init; diff --git a/src/extensions/default/OpenWithExternalApplication/node/package.json b/src/extensions/default/OpenWithExternalApplication/node/package.json new file mode 100644 index 00000000000..0e646495964 --- /dev/null +++ b/src/extensions/default/OpenWithExternalApplication/node/package.json @@ -0,0 +1,7 @@ +{ + "name": "brackets-open-external_application", + "dependencies": { + "open": "0.0.5", + "glob": "7.1.1" + } +} diff --git a/src/extensions/default/PhpTooling/CodeHintsProvider.js b/src/extensions/default/PhpTooling/CodeHintsProvider.js new file mode 100644 index 00000000000..1029c9ccacf --- /dev/null +++ b/src/extensions/default/PhpTooling/CodeHintsProvider.js @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +/* eslint-disable indent */ +/* eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + "use strict"; + + var _ = brackets.getModule("thirdparty/lodash"); + + var DefaultProviders = brackets.getModule("languageTools/DefaultProviders"), + EditorManager = brackets.getModule('editor/EditorManager'), + TokenUtils = brackets.getModule("utils/TokenUtils"), + StringMatch = brackets.getModule("utils/StringMatch"), + matcher = new StringMatch.StringMatcher({ + preferPrefixMatches: true + }); + + var phpSuperGlobalVariables = JSON.parse(require("text!phpGlobals.json")), + hintType = { + "2": "Method", + "3": "Function", + "4": "Constructor", + "6": "Variable", + "7": "Class", + "8": "Interface", + "9": "Module", + "10": "Property", + "14": "Keyword", + "21": "Constant" + }; + + function CodeHintsProvider(client) { + this.defaultCodeHintProviders = new DefaultProviders.CodeHintsProvider(client); + } + + CodeHintsProvider.prototype.setClient = function (client) { + this.defaultCodeHintProviders.setClient(client); + }; + + function setStyleAndCacheToken($hintObj, token) { + $hintObj.addClass('brackets-hints-with-type-details'); + $hintObj.data('completionItem', token); + } + + function filterWithQueryAndMatcher(hints, query) { + var matchResults = $.map(hints, function (hint) { + var searchResult = matcher.match(hint.label, query); + if (searchResult) { + for (var key in hint) { + searchResult[key] = hint[key]; + } + } + + return searchResult; + }); + + return matchResults; + } + + CodeHintsProvider.prototype.hasHints = function (editor, implicitChar) { + return this.defaultCodeHintProviders.hasHints(editor, implicitChar); + }; + + CodeHintsProvider.prototype.getHints = function (implicitChar) { + if (!this.defaultCodeHintProviders.client) { + return null; + } + + var editor = EditorManager.getActiveEditor(), + pos = editor.getCursorPos(), + docPath = editor.document.file._path, + $deferredHints = $.Deferred(), + self = this.defaultCodeHintProviders, + client = this.defaultCodeHintProviders.client; + + //Make sure the document is in sync with the server + client.notifyTextDocumentChanged({ + filePath: docPath, + fileContent: editor.document.getText() + }); + client.requestHints({ + filePath: docPath, + cursorPos: pos + }).done(function (msgObj) { + var context = TokenUtils.getInitialContext(editor._codeMirror, pos), + hints = []; + + self.query = context.token.string.slice(0, context.pos.ch - context.token.start); + if (msgObj) { + var res = msgObj.items || [], + trimmedQuery = self.query.trim(), + hasIgnoreCharacters = self.ignoreQuery.includes(implicitChar) || self.ignoreQuery.includes(trimmedQuery), + isExplicitInvokation = implicitChar === null; + + // There is a bug in Php Language Server, Php Language Server does not provide superGlobals + // Variables as completion. so these variables are being explicity put in response objects + // below code should be removed if php server fix this bug. + if((isExplicitInvokation || trimmedQuery) && !hasIgnoreCharacters) { + for(var key in phpSuperGlobalVariables) { + res.push({ + label: key, + documentation: phpSuperGlobalVariables[key].description, + detail: phpSuperGlobalVariables[key].type + }); + } + } + + var filteredHints = []; + if (hasIgnoreCharacters || (isExplicitInvokation && !trimmedQuery)) { + filteredHints = filterWithQueryAndMatcher(res, ""); + } else { + filteredHints = filterWithQueryAndMatcher(res, self.query); + } + + StringMatch.basicMatchSort(filteredHints); + filteredHints.forEach(function (element) { + var $fHint = $("") + .addClass("brackets-hints"); + + if (element.stringRanges) { + element.stringRanges.forEach(function (item) { + if (item.matched) { + $fHint.append($("") + .append(_.escape(item.text)) + .addClass("matched-hint")); + } else { + $fHint.append(_.escape(item.text)); + } + }); + } else { + $fHint.text(element.label); + } + + $fHint.data("token", element); + setStyleAndCacheToken($fHint, element); + hints.push($fHint); + }); + } + + var token = self.query; + $deferredHints.resolve({ + "hints": hints, + "enableDescription": true, + "selectInitial": token && /\S/.test(token) && isNaN(parseInt(token, 10)) // If the active token is blank then don't put default selection + }); + }).fail(function () { + $deferredHints.reject(); + }); + + return $deferredHints; + }; + + CodeHintsProvider.prototype.insertHint = function ($hint) { + return this.defaultCodeHintProviders.insertHint($hint); + }; + + CodeHintsProvider.prototype.updateHintDescription = function ($hint, $hintDescContainer) { + var $hintObj = $hint.find('.brackets-hints-with-type-details'), + token = $hintObj.data('completionItem'), + $desc = $('
'); + + if(!token) { + $hintDescContainer.empty(); + return; + } + + if (token.detail) { + if (token.detail.trim() !== '?') { + $('
' + token.detail.split('->').join(':').toString().trim() + '
').appendTo($desc).addClass("codehint-desc-type-details"); + } + } else { + if (hintType[token.kind]) { + $('
' + hintType[token.kind] + '
').appendTo($desc).addClass("codehint-desc-type-details"); + } + } + if (token.documentation) { + $('
').html(token.documentation.trim()).appendTo($desc).addClass("codehint-desc-documentation"); + } + + //To ensure CSS reflow doesn't cause a flicker. + $hintDescContainer.empty(); + $hintDescContainer.append($desc); + }; + + exports.CodeHintsProvider = CodeHintsProvider; +}); diff --git a/src/extensions/default/PhpTooling/PHPSymbolProviders.js b/src/extensions/default/PhpTooling/PHPSymbolProviders.js new file mode 100644 index 00000000000..9265bd16dda --- /dev/null +++ b/src/extensions/default/PhpTooling/PHPSymbolProviders.js @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2019 - present Adobe. 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 regexp: true */ +/*eslint no-invalid-this: 0, max-len: 0*/ +define(function (require, exports, module) { + "use strict"; + + var EditorManager = brackets.getModule("editor/EditorManager"), + QuickOpen = brackets.getModule("search/QuickOpen"), + Commands = brackets.getModule("command/Commands"), + CommandManager = brackets.getModule("command/CommandManager"), + PathConverters = brackets.getModule("languageTools/PathConverters"); + + var SymbolKind = QuickOpen.SymbolKind; + + function setClient(client) { + if (client) { + this.client = client; + } + } + + function convertRangePosToEditorPos(rangePos) { + return { + line: rangePos.line, + ch: rangePos.character + }; + } + + function SymbolInformation(label, fullPath, selectionRange, type, scope, isDocumentSymbolRequest) { + this.label = label; + this.fullPath = fullPath; + this.selectionRange = selectionRange; + this.type = type; + this.scope = scope; + this.isDocumentSymbolRequest = isDocumentSymbolRequest; + } + + function createList(list, isDocumentSymbolRequest) { + var newlist = []; + for (var i = 0; i < list.length; i++) { + var symbolInfo = list[i], + label = symbolInfo.name, + type = SymbolKind[symbolInfo.kind.toString()], + fullPath = null, + selectionRange = null, + scope = symbolInfo.containerName, + range = null; + + if (!isDocumentSymbolRequest) { + fullPath = PathConverters.uriToPath(symbolInfo.location.uri); + } else { + if (symbolInfo.selectionRange) { + range = symbolInfo.selectionRange; + selectionRange = { + from: convertRangePosToEditorPos(range.start), + to: convertRangePosToEditorPos(range.end) + }; + } + } + + if (!selectionRange) { + range = symbolInfo.location.range; + selectionRange = { + from: convertRangePosToEditorPos(range.start), + to: convertRangePosToEditorPos(range.end) + }; + } + + newlist.push(new SymbolInformation(label, fullPath, selectionRange, type, scope, isDocumentSymbolRequest)); + } + + return newlist; + } + + function transFormToSymbolList(query, matcher, results, isDocumentSymbolRequest) { + var list = createList(results, isDocumentSymbolRequest); + + // Filter and rank how good each match is + var filteredList = $.map(list, function (symbolInfo) { + var searchResult = matcher.match(symbolInfo.label, query); + if (searchResult) { + searchResult.symbolInfo = symbolInfo; + } + return searchResult; + }); + + // Sort based on ranking & basic alphabetical order + QuickOpen.basicMatchSort(filteredList); + + return filteredList; + } + + /** + * Provider for Document Symbols + */ + function DocumentSymbolsProvider(client) { + this.client = client; + } + + DocumentSymbolsProvider.prototype.setClient = setClient; + + DocumentSymbolsProvider.prototype.match = function (query) { + return query.startsWith("@"); + }; + + DocumentSymbolsProvider.prototype.search = function (query, matcher) { + if (!this.client) { + return $.Deferred().reject(); + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.documentSymbolProvider) { + return $.Deferred().reject(); + } + + var editor = EditorManager.getActiveEditor(), + docPath = editor.document.file._path, + retval = $.Deferred(); + query = query.slice(1); + + this.client.requestSymbolsForDocument({ + filePath: docPath + }).done(function (results) { + var resultList = transFormToSymbolList(query, matcher, results, true); + retval.resolve(resultList); + }); + + return retval; + }; + + DocumentSymbolsProvider.prototype.itemFocus = function (selectedItem, query, explicit) { + if (!selectedItem || (query.length < 2 && !explicit)) { + return; + } + + var range = selectedItem.symbolInfo.selectionRange; + EditorManager.getCurrentFullEditor().setSelection(range.from, range.to, true); + }; + + DocumentSymbolsProvider.prototype.itemSelect = function (selectedItem, query) { + this.itemFocus(selectedItem, query, true); + }; + + DocumentSymbolsProvider.prototype.resultsFormatter = function (item, query) { + var displayName = QuickOpen.highlightMatch(item); + query = query.slice(1); + + if (item.symbolInfo.scope) { + return "
  • " + displayName + " (" + item.symbolInfo.type + ")" + "
    " + item.symbolInfo.scope + "
  • "; + } + return "
  • " + displayName + " (" + item.symbolInfo.type + ")" + "
  • "; + }; + + /** + * Provider for Project Symbols + */ + function ProjectSymbolsProvider(client) { + this.client = client; + } + + ProjectSymbolsProvider.prototype.setClient = setClient; + + ProjectSymbolsProvider.prototype.match = function (query) { + return query.startsWith("#"); + }; + + ProjectSymbolsProvider.prototype.search = function (query, matcher) { + if (!this.client) { + return $.Deferred().reject(); + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.workspaceSymbolProvider) { + return $.Deferred().reject(); + } + + var retval = $.Deferred(); + query = query.slice(1); + + this.client.requestSymbolsForWorkspace({ + query: query + }).done(function (results) { + var resultList = transFormToSymbolList(query, matcher, results); + retval.resolve(resultList); + }); + + return retval; + }; + + ProjectSymbolsProvider.prototype.itemFocus = function (selectedItem, query, explicit) { + if (!selectedItem || (query.length < 2 && !explicit)) { + return; + } + }; + + ProjectSymbolsProvider.prototype.itemSelect = function (selectedItem, query) { + var fullPath = selectedItem.symbolInfo.fullPath, + range = selectedItem.symbolInfo.selectionRange; + + if (fullPath) { + CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { + fullPath: fullPath + }) + .done(function () { + if (range.from) { + var editor = EditorManager.getCurrentFullEditor(); + editor.setCursorPos(range.from.line, range.from.ch, true); + } + }); + } + }; + + ProjectSymbolsProvider.prototype.resultsFormatter = function (item, query) { + var displayName = QuickOpen.highlightMatch(item); + query = query.slice(1); + + if (item.symbolInfo.scope) { + return "
  • " + displayName + " (" + item.symbolInfo.type + ")" + "
    " + item.symbolInfo.scope + "

    " + item.symbolInfo.fullPath + "
  • "; + } + return "
  • " + displayName + " (" + item.symbolInfo.type + ")" + "

    " + item.symbolInfo.fullPath + "
  • "; + }; + + exports.SymbolProviders = { + DocumentSymbolsProvider: DocumentSymbolsProvider, + ProjectSymbolsProvider: ProjectSymbolsProvider + }; +}); diff --git a/src/extensions/default/PhpTooling/client.js b/src/extensions/default/PhpTooling/client.js new file mode 100644 index 00000000000..74f67d8b7ef --- /dev/null +++ b/src/extensions/default/PhpTooling/client.js @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ +/*global exports */ +/*global process */ +/*eslint-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + net = require("net"), + cp = require("child_process"), + execa = require("execa"), + semver = require('semver'), + clientName = "PhpClient", + executablePath = "", + memoryLimit = ""; + +function validatePhpExecutable(confParams) { + executablePath = confParams["executablePath"] || + (process.platform === 'win32' ? 'php.exe' : 'php'); + + memoryLimit = confParams["memoryLimit"] || '4095M'; + + return new Promise(function (resolve, reject) { + if (memoryLimit !== '-1' && !/^\d+[KMG]?$/.exec(memoryLimit)) { + reject("PHP_SERVER_MEMORY_LIMIT_INVALID"); + return; + } + + execa.stdout(executablePath, ['--version']).then(function (output) { + var matchStr = output.match(/^PHP ([^\s]+)/m); + if (!matchStr) { + reject("PHP_VERSION_INVALID"); + return; + } + var version = matchStr[1].split('-')[0]; + if (!/^\d+.\d+.\d+$/.test(version)) { + version = version.replace(/(\d+.\d+.\d+)/, '$1-'); + } + if (semver.lt(version, '7.0.0')) { + reject(["PHP_UNSUPPORTED_VERSION", version]); + return; + } + resolve(); + }).catch(function (err) { + if (err.code === 'ENOENT') { + reject("PHP_EXECUTABLE_NOT_FOUND"); + } else { + reject(["PHP_PROCESS_SPAWN_ERROR", err.code]); + console.error(err); + } + return; + }); + }); +} + +var serverOptions = function () { + return new Promise(function (resolve, reject) { + var server = net.createServer(function (socket) { + console.log('PHP process connected'); + socket.on('end', function () { + console.log('PHP process disconnected'); + }); + server.close(); + resolve({ + reader: socket, + writer: socket + }); + }); + server.listen(0, '127.0.0.1', function () { + var pathToPHP = __dirname + "/vendor/felixfbecker/language-server/bin/php-language-server.php"; + var childProcess = cp.spawn(executablePath, [ + pathToPHP, + '--tcp=127.0.0.1:' + server.address().port, + '--memory-limit=' + memoryLimit + ]); + childProcess.stderr.on('data', function (chunk) { + var str = chunk.toString(); + console.log('PHP Language Server:', str); + }); + childProcess.on('exit', function (code, signal) { + console.log( + "Language server exited " + (signal ? "from signal " + signal : "with exit code " + code) + ); + }); + return childProcess; + }); + }); + }, + options = { + serverOptions: serverOptions + }; + + +function init(domainManager) { + var client = new LanguageClient(clientName, domainManager, options); + client.addOnRequestHandler('validatePhpExecutable', validatePhpExecutable); +} + +exports.init = init; diff --git a/src/extensions/default/PhpTooling/composer.json b/src/extensions/default/PhpTooling/composer.json new file mode 100644 index 00000000000..ce39680788b --- /dev/null +++ b/src/extensions/default/PhpTooling/composer.json @@ -0,0 +1,7 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "felixfbecker/language-server": "^5.4" + } +} diff --git a/src/extensions/default/PhpTooling/main.js b/src/extensions/default/PhpTooling/main.js new file mode 100755 index 00000000000..fad20cd2429 --- /dev/null +++ b/src/extensions/default/PhpTooling/main.js @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + ClientLoader = brackets.getModule("languageTools/ClientLoader"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + ProjectManager = brackets.getModule("project/ProjectManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + LanguageManager = brackets.getModule("language/LanguageManager"), + CodeHintManager = brackets.getModule("editor/CodeHintManager"), + QuickOpen = brackets.getModule("search/QuickOpen"), + ParameterHintManager = brackets.getModule("features/ParameterHintsManager"), + JumpToDefManager = brackets.getModule("features/JumpToDefManager"), + FindReferencesManager = brackets.getModule("features/FindReferencesManager"), + CodeInspection = brackets.getModule("language/CodeInspection"), + DefaultProviders = brackets.getModule("languageTools/DefaultProviders"), + CodeHintsProvider = require("CodeHintsProvider").CodeHintsProvider, + SymbolProviders = require("PHPSymbolProviders").SymbolProviders, + DefaultEventHandlers = brackets.getModule("languageTools/DefaultEventHandlers"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), + Dialogs = brackets.getModule("widgets/Dialogs"), + DefaultDialogs = brackets.getModule("widgets/DefaultDialogs"), + Commands = brackets.getModule("command/Commands"), + CommandManager = brackets.getModule("command/CommandManager"), + StringUtils = brackets.getModule("utils/StringUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "PhpClient", + _client = null, + evtHandler, + phpConfig = { + enablePhpTooling: true, + executablePath: "php", + memoryLimit: "4095M", + validateOnType: "false" + }, + DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW = "debug.openPrefsInSplitView", + phpServerRunning = false, + serverCapabilities, + currentRootPath, + chProvider = null, + phProvider = null, + lProvider = null, + jdProvider = null, + dSymProvider = null, + pSymProvider = null, + refProvider = null, + providersRegistered = false; + + PreferencesManager.definePreference("php", "object", phpConfig, { + description: Strings.DESCRIPTION_PHP_TOOLING_CONFIGURATION + }); + + PreferencesManager.on("change", "php", function () { + var newPhpConfig = PreferencesManager.get("php"); + + if (lProvider && newPhpConfig["validateOnType"] !== phpConfig["validateOnType"]) { + lProvider._validateOnType = !(newPhpConfig["validateOnType"] === "false"); + } + if ((newPhpConfig["executablePath"] !== phpConfig["executablePath"]) + || (newPhpConfig["enablePhpTooling"] !== phpConfig["enablePhpTooling"])) { + phpConfig = newPhpConfig; + runPhpServer(); + return; + } + phpConfig = newPhpConfig; + }); + + var handleProjectOpen = function (event, directory) { + lProvider.clearExistingResults(); + if(serverCapabilities["workspace"] && serverCapabilities["workspace"]["workspaceFolders"]) { + _client.notifyProjectRootsChanged({ + foldersAdded: [directory.fullPath], + foldersRemoved: [currentRootPath] + }); + currentRootPath = directory.fullPath; + } else { + _client.restart({ + rootPath: directory.fullPath + }).done(handlePostPhpServerStart); + } + }; + + function resetClientInProviders() { + var logErr = "PhpTooling: Can't reset client for : "; + chProvider ? chProvider.setClient(_client) : console.log(logErr, "CodeHintsProvider"); + phProvider ? phProvider.setClient(_client) : console.log(logErr, "ParameterHintsProvider"); + jdProvider ? jdProvider.setClient(_client) : console.log(logErr, "JumpToDefProvider"); + dSymProvider ? dSymProvider.setClient(_client) : console.log(logErr, "DocumentSymbolsProvider"); + pSymProvider ? pSymProvider.setClient(_client) : console.log(logErr, "ProjectSymbolsProvider"); + refProvider ? refProvider.setClient(_client) : console.log(logErr, "FindReferencesProvider"); + lProvider ? lProvider.setClient(_client) : console.log(logErr, "LintingProvider"); + _client.addOnCodeInspection(lProvider.setInspectionResults.bind(lProvider)); + } + + function registerToolingProviders() { + chProvider = new CodeHintsProvider(_client), + phProvider = new DefaultProviders.ParameterHintsProvider(_client), + lProvider = new DefaultProviders.LintingProvider(_client), + jdProvider = new DefaultProviders.JumpToDefProvider(_client); + dSymProvider = new SymbolProviders.DocumentSymbolsProvider(_client); + pSymProvider = new SymbolProviders.ProjectSymbolsProvider(_client); + refProvider = new DefaultProviders.ReferencesProvider(_client); + + JumpToDefManager.registerJumpToDefProvider(jdProvider, ["php"], 0); + CodeHintManager.registerHintProvider(chProvider, ["php"], 0); + ParameterHintManager.registerHintProvider(phProvider, ["php"], 0); + FindReferencesManager.registerFindReferencesProvider(refProvider, ["php"], 0); + FindReferencesManager.setMenuItemStateForLanguage(); + CodeInspection.register(["php"], { + name: "", + scanFileAsync: lProvider.getInspectionResultsAsync.bind(lProvider) + }); + //Attach plugin for Document Symbols + QuickOpen.addQuickOpenPlugin({ + name: "PHP Document Symbols", + label: Strings.CMD_FIND_DOCUMENT_SYMBOLS + "\u2026", + languageIds: ["php"], + search: dSymProvider.search.bind(dSymProvider), + match: dSymProvider.match.bind(dSymProvider), + itemFocus: dSymProvider.itemFocus.bind(dSymProvider), + itemSelect: dSymProvider.itemSelect.bind(dSymProvider), + resultsFormatter: dSymProvider.resultsFormatter.bind(dSymProvider) + }); + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION).setEnabled(true); + //Attach plugin for Project Symbols + QuickOpen.addQuickOpenPlugin({ + name: "PHP Project Symbols", + label: Strings.CMD_FIND_PROJECT_SYMBOLS + "\u2026", + languageIds: ["php"], + search: pSymProvider.search.bind(pSymProvider), + match: pSymProvider.match.bind(pSymProvider), + itemFocus: pSymProvider.itemFocus.bind(pSymProvider), + itemSelect: pSymProvider.itemSelect.bind(pSymProvider), + resultsFormatter: pSymProvider.resultsFormatter.bind(pSymProvider) + }); + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION_PROJECT).setEnabled(true); + + _client.addOnCodeInspection(lProvider.setInspectionResults.bind(lProvider)); + + providersRegistered = true; + } + + function addEventHandlers() { + _client.addOnLogMessage(function () {}); + _client.addOnShowMessage(function () {}); + evtHandler = new DefaultEventHandlers.EventPropagationProvider(_client); + evtHandler.registerClientForEditorEvent(); + + + if (phpConfig["validateOnType"] !== "false") { + lProvider._validateOnType = true; + } + + _client.addOnProjectOpenHandler(handleProjectOpen); + } + + function validatePhpExecutable() { + var result = $.Deferred(); + + _client.sendCustomRequest({ + messageType: "brackets", + type: "validatePhpExecutable", + params: phpConfig + }).done(result.resolve).fail(result.reject); + + return result; + } + + function showErrorPopUp(err) { + if(!err) { + return; + } + var localizedErrStr = ""; + if (typeof (err) === "string") { + localizedErrStr = Strings[err]; + } else { + localizedErrStr = StringUtils.format(Strings[err[0]], err[1]); + } + if(!localizedErrStr) { + console.error("Php Tooling Error: " + err); + return; + } + var Buttons = [ + { className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, + text: Strings.CANCEL }, + { className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_DOWNLOAD, + text: Strings.OPEN_PREFERENNCES} + ]; + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.PHP_SERVER_ERROR_TITLE, + localizedErrStr, + Buttons + ).done(function (id) { + if (id === Dialogs.DIALOG_BTN_DOWNLOAD) { + if (CommandManager.get(DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW)) { + CommandManager.execute(DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW); + } else { + CommandManager.execute(Commands.CMD_OPEN_PREFERENCES); + } + } + }); + } + + function handlePostPhpServerStart() { + if (!phpServerRunning) { + phpServerRunning = true; + + if (providersRegistered) { + resetClientInProviders(); + } else { + registerToolingProviders(); + } + + addEventHandlers(); + EditorManager.off("activeEditorChange.php"); + LanguageManager.off("languageModified.php"); + } + evtHandler.handleActiveEditorChange(null, EditorManager.getActiveEditor()); + currentRootPath = ProjectManager.getProjectRoot()._path; + } + + function runPhpServer() { + if (_client && phpConfig["enablePhpTooling"]) { + validatePhpExecutable() + .done(function () { + var startFunc = _client.start.bind(_client); + if (phpServerRunning) { + startFunc = _client.restart.bind(_client); + } + currentRootPath = ProjectManager.getProjectRoot()._path; + startFunc({ + rootPath: currentRootPath + }).done(function (result) { + console.log("php Language Server started"); + serverCapabilities = result.capabilities; + handlePostPhpServerStart(); + }); + }).fail(showErrorPopUp); + } + } + + function activeEditorChangeHandler(event, current) { + if (current) { + var language = current.document.getLanguage(); + if (language.getId() === "php") { + runPhpServer(); + EditorManager.off("activeEditorChange.php"); + LanguageManager.off("languageModified.php"); + } + } + } + + function languageModifiedHandler(event, language) { + if (language && language.getId() === "php") { + runPhpServer(); + EditorManager.off("activeEditorChange.php"); + LanguageManager.off("languageModified.php"); + } + } + + function initiateService(evt, onAppReady) { + if (onAppReady) { + console.log("Php tooling: Starting the service"); + } else { + console.log("Php tooling: Something went wrong. Restarting the service"); + } + + phpServerRunning = false; + LanguageTools.initiateToolingService(clientName, clientFilePath, ['php']).done(function (client) { + _client = client; + //Attach only once + EditorManager.off("activeEditorChange.php"); + EditorManager.on("activeEditorChange.php", activeEditorChangeHandler); + //Attach only once + LanguageManager.off("languageModified.php"); + LanguageManager.on("languageModified.php", languageModifiedHandler); + activeEditorChangeHandler(null, EditorManager.getActiveEditor()); + }); + } + + AppInit.appReady(function () { + initiateService(null, true); + ClientLoader.on("languageClientModuleInitialized", initiateService); + }); + + //Only for Unit testing + exports.getClient = function() { return _client; }; +}); diff --git a/src/extensions/default/PhpTooling/package.json b/src/extensions/default/PhpTooling/package.json new file mode 100644 index 00000000000..e9ef5096e72 --- /dev/null +++ b/src/extensions/default/PhpTooling/package.json @@ -0,0 +1,14 @@ +{ + "name": "php-Tooling", + "version": "1.0.0", + "description": "Advanced Tooling support for PHP", + "author": "niteskum", + "main": "main.js", + "scripts": { + "postinstall": "composer require felixfbecker/language-server && composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs" + }, + "dependencies": { + "execa": "1.0.0", + "semver": "5.6.0" + } +} diff --git a/src/extensions/default/PhpTooling/phpGlobals.json b/src/extensions/default/PhpTooling/phpGlobals.json new file mode 100644 index 00000000000..cbb8bb501fe --- /dev/null +++ b/src/extensions/default/PhpTooling/phpGlobals.json @@ -0,0 +1,75 @@ +{ + "$GLOBALS": { + "description": "An associative array containing references to all variables which are currently defined in the global scope of the script. The variable names are the keys of the array.", + "type": "array" + }, + "$_SERVER": { + "description": "$_SERVER is an array containing information such as headers, paths, and script locations. The entries in this array are created by the web server. There is no guarantee that every web server will provide any of these; servers may omit some, or provide others not listed here. That said, a large number of these variables are accounted for in the CGI/1.1 specification, so you should be able to expect those.", + "type": "array" + }, + "$_GET": { + "description": "An associative array of variables passed to the current script via the URL parameters.", + "type": "array" + }, + "$_POST": { + "description": "An associative array of variables passed to the current script via the HTTP POST method.", + "type": "array" + }, + "$_FILES": { + "description": "An associative array of items uploaded to the current script via the HTTP POST method.", + "type": "array" + }, + "$_REQUEST": { + "description": "An associative array that by default contains the contents of $_GET, $_POST and $_COOKIE.", + "type": "array" + }, + "$_SESSION": { + "description": "An associative array containing session variables available to the current script. See the Session functions documentation for more information on how this is used.", + "type": "array" + }, + "$_ENV": { + "description": "An associative array of variables passed to the current script via the environment method. \r\n\r\nThese variables are imported into PHP\"s global namespace from the environment under which the PHP parser is running. Many are provided by the shell under which PHP is running and different systems are likely running different kinds of shells, a definitive list is impossible. Please see your shell\"s documentation for a list of defined environment variables. \r\n\r\nOther environment variables include the CGI variables, placed there regardless of whether PHP is running as a server module or CGI processor.", + "type": "array" + }, + "$_COOKIE": { + "description": "An associative array of variables passed to the current script via HTTP Cookies.", + "type": "array" + }, + "$php_errormsg": { + "description": "$php_errormsg is a variable containing the text of the last error message generated by PHP. This variable will only be available within the scope in which the error occurred, and only if the track_errors configuration option is turned on (it defaults to off).", + "type": "array" + }, + "$HTTP_RAW_POST_DATA": { + "description": "$HTTP_RAW_POST_DATA contains the raw POST data. See always_populate_raw_post_data", + "type": "array" + }, + "$http_response_header": { + "description": "The $http_response_header array is similar to the get_headers() function. When using the HTTP wrapper, $http_response_header will be populated with the HTTP response headers. $http_response_header will be created in the local scope.", + "type": "array" + }, + "$argc": { + "description": "Contains the number of arguments passed to the current script when running from the command line.", + "type": "array" + }, + "$argv": { + "description": "Contains an array of all the arguments passed to the script when running from the command line.", + "type": "array" + }, + "$this": { + "description": "Refers to the current object", + "type": "array" + }, + "parent": { + "description": "", + "type": "" + }, + "self": { + "description": "", + "type": "" + }, + "_destruct": { + "description": "", + "type": "" + } + +} \ No newline at end of file diff --git a/src/extensions/default/PhpTooling/unittest-files/mac/invalidphp b/src/extensions/default/PhpTooling/unittest-files/mac/invalidphp new file mode 100755 index 00000000000..487eecd1068 Binary files /dev/null and b/src/extensions/default/PhpTooling/unittest-files/mac/invalidphp differ diff --git a/src/extensions/default/PhpTooling/unittest-files/test/test1.php b/src/extensions/default/PhpTooling/unittest-files/test/test1.php new file mode 100644 index 00000000000..9bd97684a9c --- /dev/null +++ b/src/extensions/default/PhpTooling/unittest-files/test/test1.php @@ -0,0 +1,2 @@ + diff --git a/src/extensions/default/PhpTooling/unittest-files/test/test3.php b/src/extensions/default/PhpTooling/unittest-files/test/test3.php new file mode 100644 index 00000000000..7dc1d365bd2 --- /dev/null +++ b/src/extensions/default/PhpTooling/unittest-files/test/test3.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/extensions/default/PhpTooling/unittest-files/win/invalidphp.exe b/src/extensions/default/PhpTooling/unittest-files/win/invalidphp.exe new file mode 100755 index 00000000000..7f2c946e646 Binary files /dev/null and b/src/extensions/default/PhpTooling/unittest-files/win/invalidphp.exe differ diff --git a/src/extensions/default/PhpTooling/unittests.js b/src/extensions/default/PhpTooling/unittests.js new file mode 100644 index 00000000000..53b915227da --- /dev/null +++ b/src/extensions/default/PhpTooling/unittests.js @@ -0,0 +1,763 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ +/*global describe, runs, beforeEach, it, expect, waitsFor, waitsForDone, beforeFirst, afterLast */ +define(function (require, exports, module) { + 'use strict'; + + var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), + Strings = brackets.getModule("strings"), + FileUtils = brackets.getModule("file/FileUtils"), + StringUtils = brackets.getModule("utils/StringUtils"), + StringMatch = brackets.getModule("utils/StringMatch"); + + var extensionRequire, + phpToolingExtension, + testWindow, + $, + PreferencesManager, + CodeInspection, + DefaultProviders, + CodeHintsProvider, + SymbolProviders, + EditorManager, + testEditor, + testFolder = FileUtils.getNativeModuleDirectoryPath(module) + "/unittest-files/", + testFile1 = "test1.php", + testFile2 = "test2.php", + testFile4 = "test4.php"; + + describe("PhpTooling", function () { + + beforeFirst(function () { + + // Create a new window that will be shared by ALL tests in this spec. + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + testWindow = w; + $ = testWindow.$; + var brackets = testWindow.brackets; + extensionRequire = brackets.test.ExtensionLoader.getRequireContextForExtension("PhpTooling"); + phpToolingExtension = extensionRequire("main"); + }); + }); + + afterLast(function () { + waitsForDone(phpToolingExtension.getClient().stop(), "stoping php server"); + testEditor = null; + testWindow = null; + brackets = null; + EditorManager = null; + SpecRunnerUtils.closeTestWindow(); + }); + + + beforeEach(function () { + EditorManager = testWindow.brackets.test.EditorManager; + PreferencesManager = testWindow.brackets.test.PreferencesManager; + CodeInspection = testWindow.brackets.test.CodeInspection; + CodeInspection.toggleEnabled(true); + DefaultProviders = testWindow.brackets.getModule("languageTools/DefaultProviders"); + CodeHintsProvider = extensionRequire("CodeHintsProvider"); + SymbolProviders = extensionRequire("PHPSymbolProviders").SymbolProviders; + }); + + /** + * Does a busy wait for a given number of milliseconds + * @param {Number} milliSeconds - number of milliSeconds to wait + */ + function waitForMilliSeconds(milliSeconds) { + var flag = false; + + setTimeout(function () { + flag = true; + }, milliSeconds); + + waitsFor(function () { + return flag; + }, "This should not fail. Please check the timeout values.", + milliSeconds + 10); // We give 10 milliSeconds as grace period + } + + /** + * Check the presence of a Button in Error Prompt + * @param {String} btnId - "CANCEL" or "OPEN" + */ + function checkPopUpButton(clickbtnId) { + var doc = $(testWindow.document), + errorPopUp = doc.find(".error-dialog.instance"), + btn = errorPopUp.find('.dialog-button'); + + // Test if the update bar button has been displayed. + expect(btn.length).toBe(2); + if (clickbtnId) { + clickButton(clickbtnId); + } + } + + /** + * Check the presence of a Button in Error Prompt + * @param {String} btnId - Button OPEN or Cancel Button + */ + function clickButton(btnId) { + var doc = $(testWindow.document), + errorPopUp = doc.find(".error-dialog.instance"), + btn = errorPopUp.find('.dialog-button'), + openBtn, + cancelBtn, + clickBtn; + if (btn[0].classList.contains("primary")) { + openBtn = btn[0]; + cancelBtn = btn[1]; + } + + if (btn[1].classList.contains("primary")) { + openBtn = btn[1]; + cancelBtn = btn[0]; + } + clickBtn = cancelBtn; + + if(btnId === "OPEN") { + clickBtn = openBtn; + } + + if(clickBtn) { + clickBtn.click(); + waitForMilliSeconds(3000); + runs(function() { + expect(doc.find(".error-dialog.instance").length).toBe(0); + }); + } + } + + /** + * Check the presence of Error Prompt String on Brackets Window + * @param {String} title - Title String Which will be matched with Update Bar heading. + * @param {String} description - description String Which will be matched with Update Bar description. + */ + function checkPopUpString(title, titleDescription) { + var doc = $(testWindow.document), + errorPopUp = doc.find(".error-dialog.instance"), + heading = errorPopUp.find('.dialog-title'), + description = errorPopUp.find('.dialog-message'); + + // Test if the update bar has been displayed. + //expect(errorPopUp.length).toBe(1); + if (title) { + expect(heading.text()).toBe(title); + } + if (titleDescription) { + expect(description.text()).toBe(titleDescription); + } + } + + function toggleDiagnosisResults(visible) { + var doc = $(testWindow.document), + problemsPanel = doc.find("#problems-panel"), + statusInspection = $("#status-inspection"); + statusInspection.triggerHandler("click"); + expect(problemsPanel.is(":visible")).toBe(visible); + } + + /** + * Wait for the editor to change positions, such as after a jump to + * definition has been triggered. Will timeout after 3 seconds + * + * @param {{line:number, ch:number}} oldLocation - the original line/col + * @param {Function} callback - the callback to apply once the editor has changed position + */ + function _waitForJump(jumpPromise, callback) { + var cursor = null, + complete = false; + + jumpPromise.done(function () { + complete = true; + }); + + waitsFor(function () { + var activeEditor = EditorManager.getActiveEditor(); + cursor = activeEditor.getCursorPos(); + return complete; + }, "Expected jump did not occur", 3000); + + runs(function () { callback(cursor); }); + } + + /* + * Expect a given list of hints to be present in a given hint + * response object + * + * @param {Object + jQuery.Deferred} hintObj - a hint response object, + * possibly deferred + * @param {Array.} expectedHints - a list of hints that should be + * present in the hint response + */ + function expecthintsPresent(expectedHints) { + var hintObj = (new CodeHintsProvider.CodeHintsProvider(phpToolingExtension.getClient())).getHints(null); + _waitForHints(hintObj, function (hintList) { + expect(hintList).toBeTruthy(); + expectedHints.forEach(function (expectedHint) { + expect(_indexOf(hintList, expectedHint)).not.toBe(-1); + }); + }); + } + + /* + * Return the index at which hint occurs in hintList + * + * @param {Array.} hintList - the list of hints + * @param {string} hint - the hint to search for + * @return {number} - the index into hintList at which the hint occurs, + * or -1 if it does not + */ + function _indexOf(hintList, hint) { + var index = -1, + counter = 0; + + for (counter; counter < hintList.length; counter++) { + if (hintList[counter].data("token").label === hint) { + index = counter; + break; + } + } + return index; + } + + /* + * Wait for a hint response object to resolve, then apply a callback + * to the result + * + * @param {Object + jQuery.Deferred} hintObj - a hint response object, + * possibly deferred + * @param {Function} callback - the callback to apply to the resolved + * hint response object + */ + function _waitForHints(hintObj, callback) { + var complete = false, + hintList = null; + + if (hintObj.hasOwnProperty("hints")) { + complete = true; + hintList = hintObj.hints; + } else { + hintObj.done(function (obj) { + complete = true; + hintList = obj.hints; + }); + } + + waitsFor(function () { + return complete; + }, "Expected hints did not resolve", 3000); + + runs(function () { callback(hintList); }); + } + + /** + * Show a function hint based on the code at the cursor. Verify the + * hint matches the passed in value. + * + * @param {Array<{name: string, type: string, isOptional: boolean}>} + * expectedParams - array of records, where each element of the array + * describes a function parameter. If null, then no hint is expected. + * @param {number} expectedParameter - the parameter at cursor. + */ + function expectParameterHint(expectedParams, expectedParameter) { + var requestStatus = null; + var request, + complete = false; + runs(function () { + request = (new DefaultProviders.ParameterHintsProvider(phpToolingExtension.getClient())) + .getParameterHints(); + request.done(function (status) { + complete = true; + requestStatus = status; + }).fail(function(){ + complete = true; + }); + }); + + waitsFor(function () { + return complete; + }, "Expected Parameter hints did not resolve", 3000); + + if (expectedParams === null) { + expect(requestStatus).toBe(null); + return; + } + + function expectHint(hint) { + var params = hint.parameters, + n = params.length, + i; + + // compare params to expected params + expect(params.length).toBe(expectedParams.length); + expect(hint.currentIndex).toBe(expectedParameter); + + for (i = 0; i < n; i++) { + expect(params[i].label).toBe(expectedParams[i]); + } + + } + runs(function() { + expectHint(requestStatus); + }); + } + + /** + * Show the document/project symbols for a language type. + * + * @param {SymbolProvider} provider The symbol provider to use for the request. + * @param {string} query The query string for the request. + * @param {Array} expectedSymbols Expected results for the request. + */ + function expectSymbols(provider, query, expectedSymbols) { + var requestStatus = null; + var request, + matcher; + + runs(function () { + matcher = new StringMatch.StringMatcher(); + request = new provider(phpToolingExtension.getClient()).search(query, matcher); + request.done(function (status) { + requestStatus = status; + }); + + waitsForDone(request, "Expected Symbols did not resolve", 3000); + }); + + if (expectedSymbols === []) { + expect(requestStatus).toBe([]); + return; + } + + function matchSymbols(symbols) { + var n = symbols.length > 4 ? 4 : symbols.length, + i; + + for (i = 0; i < n; i++) { + var symbolInfo = symbols[i].symbolInfo; + expect(symbolInfo.label).toBe(expectedSymbols[i].label); + expect(symbolInfo.type).toBe(expectedSymbols[i].type); + expect(symbolInfo.scope).toBe(expectedSymbols[i].scope); + + if (expectedSymbols[i].fullPath === null) { + expect(symbolInfo.fullPath).toBe(null); + } else { + expect(symbolInfo.fullPath.includes(expectedSymbols[i].fullPath)).toBe(true); + } + } + + } + runs(function() { + matchSymbols(requestStatus); + }); + } + + /** + * Trigger a jump to definition, and verify that the editor jumped to + * the expected location. The new location is the variable definition + * or function definition of the variable or function at the current + * cursor location. Jumping to the new location will cause a new editor + * to be opened or open an existing editor. + * + * @param {{line:number, ch:number, file:string}} expectedLocation - the + * line, column, and optionally the new file the editor should jump to. If the + * editor is expected to stay in the same file, then file may be omitted. + */ + function editorJumped(expectedLocation) { + var jumpPromise = (new DefaultProviders.JumpToDefProvider(phpToolingExtension.getClient())).doJumpToDef(); + + _waitForJump(jumpPromise, function (newCursor) { + expect(newCursor.line).toBe(expectedLocation.line); + expect(newCursor.ch).toBe(expectedLocation.ch); + if (expectedLocation.file) { + var activeEditor = EditorManager.getActiveEditor(); + expect(activeEditor.document.file.name).toBe(expectedLocation.file); + } + }); + + } + + function expectReferences(referencesExpected) { + var refPromise, + results = null, + complete = false; + runs(function () { + refPromise = (new DefaultProviders.ReferencesProvider(phpToolingExtension.getClient())).getReferences(); + refPromise.done(function (resp) { + complete = true; + results = resp; + }).fail(function(){ + complete = true; + }); + }); + + waitsFor(function () { + return complete; + }, "Expected Reference Promise did not resolve", 3000); + + if(referencesExpected === null) { + expect(results).toBeNull(); + return; + } + + runs(function() { + expect(results.numFiles).toBe(referencesExpected.numFiles); + expect(results.numMatches).toBe(referencesExpected.numMatches); + expect(results.allResultsAvailable).toBe(referencesExpected.allResultsAvailable); + expect(results.results).not.toBeNull(); + for(var key in results.keys) { + expect(results.results.key).toBe(referencesExpected.results.key); + } + }); + } + + /** + * Check the presence of Error Prompt on Brackets Window + */ + function checkErrorPopUp() { + var doc = $(testWindow.document), + errorPopUp = doc.find(".error-dialog.instance"), + errorPopUpHeader = errorPopUp.find(".modal-header"), + errorPopUpBody = errorPopUp.find(".modal-body"), + errorPopUpFooter = errorPopUp.find(".modal-footer"), + errorPopUpPresent = false; + + runs(function () { + expect(errorPopUp.length).toBe(1); + expect(errorPopUpHeader).not.toBeNull(); + expect(errorPopUpBody).not.toBeNull(); + expect(errorPopUpFooter).not.toBeNull(); + }); + + if (errorPopUp && errorPopUp.length > 0) { + errorPopUpPresent = true; + } + return errorPopUpPresent; + } + + it("phpTooling Exiension should be loaded Successfully", function () { + waitForMilliSeconds(5000); + runs(function () { + expect(phpToolingExtension).not.toBeNull(); + }); + }); + + it("should attempt to start php server and fail due to lower version of php", function () { + var phpExecutable = testWindow.brackets.platform === "mac" ? "/mac/invalidphp" : "/win/invalidphp"; + PreferencesManager.set("php", { + "executablePath": testFolder + phpExecutable + }, { + locations: {scope: "session"} + }); + waitForMilliSeconds(5000); + runs(function () { + checkErrorPopUp(); + checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, + StringUtils.format(Strings.PHP_UNSUPPORTED_VERSION, "5.6.30")); + checkPopUpButton("CANCEL"); + }); + }); + + it("should attempt to start php server and fail due to invaild executable", function () { + PreferencesManager.set("php", {"executablePath": "/invalidPath/php"}, {locations: {scope: "session"}}); + waitForMilliSeconds(5000); + runs(function () { + checkErrorPopUp(); + checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_EXECUTABLE_NOT_FOUND); + checkPopUpButton("CANCEL"); + }); + }); + + it("should attempt to start php server and fail due to invaild memory limit in prefs settings", function () { + PreferencesManager.set("php", {"memoryLimit": "invalidValue"}, {locations: {scope: "session"}}); + waitForMilliSeconds(5000); + runs(function () { + checkErrorPopUp(); + checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_SERVER_MEMORY_LIMIT_INVALID); + checkPopUpButton("CANCEL"); + }); + + runs(function () { + SpecRunnerUtils.loadProjectInTestWindow(testFolder + "test"); + }); + }); + + it("should attempt to start php server and success", function () { + PreferencesManager.set("php", {"memoryLimit": "4095M"}, {locations: {scope: "session"}}); + + waitsForDone(SpecRunnerUtils.openProjectFiles([testFile1]), "open test file: " + testFile1); + waitForMilliSeconds(5000); + runs(function () { + toggleDiagnosisResults(false); + toggleDiagnosisResults(true); + }); + }); + + it("should filter hints by query", function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([testFile2]), "open test file: " + testFile2); + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 15, ch: 3 }); + expecthintsPresent(["$A11", "$A12", "$A13"]); + }); + }); + + it("should show inbuilt functions in hints", function () { + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 17, ch: 2 }); + expecthintsPresent(["fopen", "for", "foreach"]); + }); + }); + + it("should show static global variables in hints", function () { + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 20, ch: 1 }); + expecthintsPresent(["$_COOKIE", "$_ENV"]); + }); + }); + + it("should not show parameter hints", function () { + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 25, ch: 5 }); + expectParameterHint(null); + }); + }); + + it("should show no parameter as a hint", function () { + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 27, ch: 19 }); + expectParameterHint([], 0); + }); + }); + + it("should show parameters hints", function () { + runs(function() { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos({ line: 26, ch: 9 }); + expectParameterHint([ + "string $filename", + "string $mode", + "bool $use_include_path = null", + "resource $context = null"], 1); + }); + }); + + it("should not show any references", function () { + var start = { line: 6, ch: 4 }; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + expectReferences(null); + }); + }); + + it("should show reference present in single file", function () { + var start = { line: 22, ch: 18 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 27, ch: 0}, + end: {line: 27, ch: 18}, + line: "watchparameterhint()" + } + ] + }; + expectReferences({ + numFiles: 1, + numMatches: 1, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php"], + results: results + }); + }); + }); + + it("should show references present in single file", function () { + var start = { line: 34, ch: 8 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 34, ch: 0}, + end: {line: 34, ch: 17}, + line: "watchReferences();" + }, + { + start: {line: 36, ch: 0}, + end: {line: 36, ch: 17}, + line: "watchReferences();" + } + ] + }; + expectReferences({ + numFiles: 1, + numMatches: 2, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php"], + results: results + }); + }); + }); + + it("should show references present in multiple files", function () { + var start = { line: 39, ch: 21 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 34, ch: 0}, + end: {line: 34, ch: 26}, + line: "watchReferences();" + }, + { + start: {line: 36, ch: 0}, + end: {line: 36, ch: 26}, + line: "watchReferences();" + } + ] + }; + results[testFolder + "test/test3.php"] = {matches: [ + { + start: {line: 11, ch: 0}, + end: {line: 11, ch: 26}, + line: "watchReferences();" + } + ] + }; + expectReferences({ + numFiles: 2, + numMatches: 3, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php", testFolder + "test/test3.php"], + results: results + }); + }); + }); + + it("should jump to earlier defined variable", function () { + var start = { line: 4, ch: 2 }; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + editorJumped({line: 2, ch: 0}); + }); + }); + + it("should jump to class declared in other module file", function () { + var start = { line: 9, ch: 11 }; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + editorJumped({line: 4, ch: 0, file: "test3.php"}); + }); + }); + + it("should fetch document symbols for a given file", function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([testFile4]), "open test file: " + testFile4); + runs(function () { + var provider = SymbolProviders.DocumentSymbolsProvider, + query = "@", + expectedSymbols = [ + { + "label": "constantValue", + "fullPath": null, + "type": "Constant", + "scope": "MyClass" + }, + { + "label": "MyClass", + "fullPath": null, + "type": "Class", + "scope": "" + }, + { + "label": "publicFunction", + "fullPath": null, + "type": "Method", + "scope": "MyClass" + }, + { + "label": "publicValue", + "fullPath": null, + "type": "Property", + "scope": "MyClass" + } + ]; + expectSymbols(provider, query, expectedSymbols); + }); + }); + + it("should fetch no document symbols for a given file", function () { + waitsForDone(SpecRunnerUtils.openProjectFiles([testFile1]), "open test file: " + testFile1); + runs(function () { + var provider = SymbolProviders.DocumentSymbolsProvider, + query = "@", + expectedSymbols = []; + expectSymbols(provider, query, expectedSymbols); + }); + }); + + it("should fetch project symbols for a given file", function () { + runs(function () { + var provider = SymbolProviders.ProjectSymbolsProvider, + query = "#as", + expectedSymbols = [ + { + "label": "MyClass", + "fullPath": "test4.php", + "type": "Class", + "scope": "" + }, + { + "label": "TestCase", + "fullPath": "test2.php", + "type": "Class", + "scope": "test" + } + ]; + expectSymbols(provider, query, expectedSymbols); + }); + }); + }); +}); diff --git a/src/extensions/default/RemoteFileAdapter/RemoteFile.js b/src/extensions/default/RemoteFileAdapter/RemoteFile.js new file mode 100644 index 00000000000..4baf4529edd --- /dev/null +++ b/src/extensions/default/RemoteFileAdapter/RemoteFile.js @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemError = brackets.getModule("filesystem/FileSystemError"), + FileSystemStats = brackets.getModule("filesystem/FileSystemStats"); + + var SESSION_START_TIME = new Date(); + + /** + * Create a new file stat. See the FileSystemStats class for more details. + * + * @param {!string} fullPath The full path for this File. + * @return {FileSystemStats} stats. + */ + function _getStats(uri) { + return new FileSystemStats({ + isFile: true, + mtime: SESSION_START_TIME.toISOString(), + size: 0, + realPath: uri, + hash: uri + }); + } + + function _getFileName(filePath) { + var fileName = filePath.split('/').pop(); + + if (!fileName.trim()) { + fileName = filePath.trim().slice(0, -1); + fileName = fileName.split('/').pop(); + } + + return fileName; + } + + /** + * Model for a RemoteFile. + * + * This class should *not* be instantiated directly. Use FileSystem.getFileForPath + * + * See the FileSystem class for more details. + * + * @constructor + * @param {!string} fullPath The full path for this File. + * @param {!FileSystem} fileSystem The file system associated with this File. + */ + function RemoteFile(protocol, fullPath, fileSystem) { + this._isFile = true; + this._isDirectory = false; + this.readOnly = true; + this._path = fullPath; + this._stat = _getStats(fullPath); + this._id = fullPath; + this._name = _getFileName(fullPath); + this._fileSystem = fileSystem; + this.donotWatch = true; + this.protocol = protocol; + this.encodedPath = fullPath; + } + + // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters + Object.defineProperties(RemoteFile.prototype, { + "fullPath": { + get: function () { return this._path; }, + set: function () { throw new Error("Cannot set fullPath"); } + }, + "name": { + get: function () { return this._name; }, + set: function () { throw new Error("Cannot set name"); } + }, + "parentPath": { + get: function () { return this._parentPath; }, + set: function () { throw new Error("Cannot set parentPath"); } + }, + "id": { + get: function () { return this._id; }, + set: function () { throw new Error("Cannot set id"); } + }, + "isFile": { + get: function () { return this._isFile; }, + set: function () { throw new Error("Cannot set isFile"); } + }, + "isDirectory": { + get: function () { return this._isDirectory; }, + set: function () { throw new Error("Cannot set isDirectory"); } + }, + "_impl": { + get: function () { return this._fileSystem._impl; }, + set: function () { throw new Error("Cannot set _impl"); } + } + }); + + /** + * Helpful toString for debugging and equality check purposes + */ + RemoteFile.prototype.toString = function () { + return "[RemoteFile " + this._path + "]"; + }; + + /** + * Returns the stats for the remote entry. + * + * @param {function (?string, FileSystemStats=)} callback Callback with a + * FileSystemError string or FileSystemStats object. + */ + RemoteFile.prototype.stat = function (callback) { + if (this._stat) { + callback(null, this._stat); + } else { + callback(FileSystemError.NOT_FOUND); + } + }; + + RemoteFile.prototype.constructor = RemoteFile; + + /** + * Cached contents of this file. This value is nullable but should NOT be undefined. + * @private + * @type {?string} + */ + RemoteFile.prototype._contents = null; + + + /** + * @private + * @type {?string} + */ + RemoteFile.prototype._encoding = "utf8"; + + /** + * @private + * @type {?bool} + */ + RemoteFile.prototype._preserveBOM = false; + + + /** + * Clear any cached data for this file. Note that this explicitly does NOT + * clear the file's hash. + * @private + */ + RemoteFile.prototype._clearCachedData = function () { + // no-op + }; + + /** + * Reads a remote file. + * + * @param {Object=} options Currently unused. + * @param {function (?string, string=, FileSystemStats=)} callback Callback that is passed the + * FileSystemError string or the file's contents and its stats. + */ + RemoteFile.prototype.read = function (options, callback) { + if (typeof (options) === "function") { + callback = options; + } + this._encoding = "utf8"; + + if (this._contents !== null && this._stat) { + callback(null, this._contents, this._encoding, this._stat); + return; + } + + var self = this; + $.ajax({ + url: this.fullPath + }) + .done(function (data) { + self._contents = data; + callback(null, data, self._encoding, self._stat); + }) + .fail(function (e) { + callback(FileSystemError.NOT_FOUND); + }); + }; + + /** + * Write a file. + * + * @param {string} data Data to write. + * @param {object=} options Currently unused. + * @param {function (?string, FileSystemStats=)=} callback Callback that is passed the + * FileSystemError string or the file's new stats. + */ + RemoteFile.prototype.write = function (data, encoding, callback) { + if (typeof (encoding) === "function") { + callback = encoding; + } + callback(FileSystemError.NOT_FOUND); + }; + + RemoteFile.prototype.exists = function (callback) { + callback(null, true); + }; + + RemoteFile.prototype.unlink = function (callback) { + callback(FileSystemError.NOT_FOUND); + }; + + RemoteFile.prototype.rename = function (newName, callback) { + callback(FileSystemError.NOT_FOUND); + }; + + RemoteFile.prototype.moveToTrash = function (callback) { + callback(FileSystemError.NOT_FOUND); + }; + + // Export this class + module.exports = RemoteFile; +}); diff --git a/src/extensions/default/RemoteFileAdapter/main.js b/src/extensions/default/RemoteFileAdapter/main.js new file mode 100644 index 00000000000..5feca37ce8b --- /dev/null +++ b/src/extensions/default/RemoteFileAdapter/main.js @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018 - 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var AppInit = brackets.getModule("utils/AppInit"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + QuickOpen = brackets.getModule("search/QuickOpen"), + PathUtils = brackets.getModule("thirdparty/path-utils/path-utils"), + CommandManager = brackets.getModule("command/CommandManager"), + Commands = brackets.getModule("command/Commands"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + WorkingSetView = brackets.getModule("project/WorkingSetView"), + MainViewManager = brackets.getModule("view/MainViewManager"), + Menus = brackets.getModule("command/Menus"), + RemoteFile = require("RemoteFile"); + + var HTTP_PROTOCOL = "http:", + HTTPS_PROTOCOL = "https:"; + + ExtensionUtils.loadStyleSheet(module, "styles.css"); + + function protocolClassProvider(data) { + if (data.fullPath.startsWith("http://")) { + return "http"; + } + + if (data.fullPath.startsWith("https://")) { + return "https"; + } + + return ""; + } + + /** + * Disable context menus which are not useful for remote file + */ + function _setMenuItemsVisible() { + var file = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE), + cMenuItems = [Commands.FILE_SAVE, Commands.FILE_RENAME, Commands.NAVIGATE_SHOW_IN_FILE_TREE, Commands.NAVIGATE_SHOW_IN_OS], + // Enable menu options when no file is present in active pane + enable = !file || (file.constructor.name !== "RemoteFile"); + + // Enable or disable commands based on whether the file is a remoteFile or not. + cMenuItems.forEach(function (item) { + CommandManager.get(item).setEnabled(enable); + }); + } + + AppInit.htmlReady(function () { + + Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible); + MainViewManager.on("currentFileChange", _setMenuItemsVisible); + + var protocolAdapter = { + priority: 0, // Default priority + fileImpl: RemoteFile, + canRead: function (filePath) { + return true; // Always claim true, we are the default adpaters + } + }; + + // Register the custom object as HTTP and HTTPS protocol adapter + FileSystem.registerProtocolAdapter(HTTP_PROTOCOL, protocolAdapter); + FileSystem.registerProtocolAdapter(HTTPS_PROTOCOL, protocolAdapter); + + // Register as quick open plugin for file URI's having HTTP:|HTTPS: protocol + QuickOpen.addQuickOpenPlugin( + { + name: "Remote file URI input", + languageIds: [], // for all language modes + search: function () { + return $.Deferred().resolve([arguments[0]]); + }, + match: function (query) { + var protocol = PathUtils.parseUrl(query).protocol; + return [HTTP_PROTOCOL, HTTPS_PROTOCOL].indexOf(protocol) !== -1; + }, + itemFocus: function (query) { + }, // no op + itemSelect: function () { + CommandManager.execute(Commands.FILE_OPEN, {fullPath: arguments[0]}); + } + } + ); + + WorkingSetView.addClassProvider(protocolClassProvider); + }); + +}); diff --git a/src/extensions/default/RemoteFileAdapter/styles.css b/src/extensions/default/RemoteFileAdapter/styles.css new file mode 100644 index 00000000000..0f62bf391b2 --- /dev/null +++ b/src/extensions/default/RemoteFileAdapter/styles.css @@ -0,0 +1,21 @@ +.http a:after, +.https a:after { + margin-left: 5px; + border: 1px solid; + border-radius: 2px; + padding: 0px 5px; + font-size: 11px; + color: #adb9bd; +} + +.http a:after { + content: "http"; +} + +.https a:after { + content: "https"; +} + + + + diff --git a/src/extensions/default/RemoteFileAdapter/unittests.js b/src/extensions/default/RemoteFileAdapter/unittests.js new file mode 100644 index 00000000000..e2fc2a6a8df --- /dev/null +++ b/src/extensions/default/RemoteFileAdapter/unittests.js @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2013 - 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. + * + */ + +/*global describe, it, expect, beforeEach, afterEach, runs, waitsForDone, spyOn */ + +define(function (require, exports, module) { + "use strict"; + + var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), + FileUtils = brackets.getModule("file/FileUtils"), + CommandManager, + Commands, + Dialogs, + EditorManager, + DocumentManager, + MainViewManager, + FileSystem; + + var REMOTE_FILE_PATH = "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css", + INVALID_REMOTE_FILE_PATH = "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/invalid.min.css"; + + + + describe("RemoteFileAdapter", function () { + var testWindow, + $, + brackets; + + function createRemoteFile(filePath) { + return CommandManager.execute(Commands.FILE_OPEN, {fullPath: filePath}); + } + + function deleteCurrentRemoteFile() { + CommandManager.execute(Commands.FILE_DELETE); + } + + function saveRemoteFile() { + CommandManager.execute(Commands.FILE_SAVE); + } + + function renameRemoteFile(filePath) { + CommandManager.execute(Commands.FILE_RENAME); + } + + function closeRemoteFile(filePath) { + CommandManager.execute(Commands.FILE_CLOSE, {fullPath: filePath}); + } + + beforeEach(function () { + runs(function () { + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + testWindow = w; + $ = testWindow.$; + brackets = testWindow.brackets; + DocumentManager = testWindow.brackets.test.DocumentManager; + MainViewManager = testWindow.brackets.test.MainViewManager; + CommandManager = testWindow.brackets.test.CommandManager; + EditorManager = testWindow.brackets.test.EditorManager; + Dialogs = testWindow.brackets.test.Dialogs; + Commands = testWindow.brackets.test.Commands; + FileSystem = testWindow.brackets.test.FileSystem; + }); + }); + }); + + afterEach(function () { + testWindow = null; + $ = null; + brackets = null; + EditorManager = null; + SpecRunnerUtils.closeTestWindow(); + }); + + + it("Open/close remote https file", function () { + createRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(1); + closeRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(0); + }); + }); + }); + + it("Open invalid remote file", function () { + spyOn(Dialogs, 'showModalDialog').andCallFake(function (dlgClass, title, message, buttons) { + console.warn(title, message); + return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; + }); + createRemoteFile(INVALID_REMOTE_FILE_PATH).always(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(0); + expect(Dialogs.showModalDialog).toHaveBeenCalled(); + expect(Dialogs.showModalDialog.callCount).toBe(1); + }); + }); + + it("Save remote file", function () { + createRemoteFile(REMOTE_FILE_PATH).done(function () { + spyOn(Dialogs, 'showModalDialog').andCallFake(function (dlgClass, title, message, buttons) { + console.warn(title, message); + return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; + }); + saveRemoteFile(); + expect(Dialogs.showModalDialog).toHaveBeenCalled(); + expect(Dialogs.showModalDialog.callCount).toBe(1); + closeRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(0); + }); + }); + }); + + it("Delete remote file", function () { + createRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(1); + spyOn(Dialogs, 'showModalDialog').andCallFake(function (dlgClass, title, message, buttons) { + console.warn(title, message); + return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; + }); + deleteCurrentRemoteFile(); + expect(Dialogs.showModalDialog).toHaveBeenCalled(); + expect(Dialogs.showModalDialog.callCount).toBe(1); + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(1); + closeRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(0); + }); + }); + }); + + it("Rename remote file", function () { + createRemoteFile(REMOTE_FILE_PATH).done(function () { + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(1); + spyOn(Dialogs, 'showModalDialog').andCallFake(function (dlgClass, title, message, buttons) { + console.warn(title, message); + return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; + }); + renameRemoteFile(); + expect(Dialogs.showModalDialog).toHaveBeenCalled(); + expect(Dialogs.showModalDialog.callCount).toBe(1); + expect(MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE).length).toEqual(1); + }); + }); + }); +}); diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js new file mode 100644 index 00000000000..8b9470a959c --- /dev/null +++ b/src/features/FindReferencesManager.js @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var AppInit = require("utils/AppInit"), + CommandManager = require("command/CommandManager"), + MainViewManager = require("view/MainViewManager"), + LanguageManager = require("language/LanguageManager"), + DocumentManager = require("document/DocumentManager"), + Commands = require("command/Commands"), + EditorManager = require("editor/EditorManager"), + ProjectManager = require("project/ProjectManager"), + ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler, + SearchResultsView = require("search/SearchResultsView").SearchResultsView, + SearchModel = require("search/SearchModel").SearchModel, + Strings = require("strings"); + + var _providerRegistrationHandler = new ProviderRegistrationHandler(), + registerFindReferencesProvider = _providerRegistrationHandler.registerProvider.bind( + _providerRegistrationHandler + ), + removeFindReferencesProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + + var searchModel = new SearchModel(), + _resultsView; + + function _getReferences(provider, hostEditor, pos) { + var result = new $.Deferred(); + + if(!provider) { + return result.reject(); + } + + provider.getReferences(hostEditor, pos) + .done(function (rcvdObj) { + + searchModel.results = rcvdObj.results; + searchModel.numFiles = rcvdObj.numFiles; + searchModel.numMatches = rcvdObj.numMatches; + searchModel.allResultsAvailable = true; + searchModel.setQueryInfo({query: rcvdObj.queryInfo, caseSensitive: true, isRegExp: false}); + result.resolve(); + }).fail(function (){ + result.reject(); + }); + return result.promise(); + + } + + function _openReferencesPanel() { + var editor = EditorManager.getActiveEditor(), + pos = editor ? editor.getCursorPos() : null, + referencesPromise, + result = new $.Deferred(), + errorMsg = Strings.REFERENCES_NO_RESULTS, + referencesProvider; + + var language = editor.getLanguageForSelection(), + enabledProviders = _providerRegistrationHandler.getProvidersForLanguageId(language.getId()); + + enabledProviders.some(function (item, index) { + if (item.provider.hasReferences(editor)) { + referencesProvider = item.provider; + return true; + } + }); + + referencesPromise = _getReferences(referencesProvider, editor, pos); + + // If one of them will provide a widget, show it inline once ready + if (referencesPromise) { + referencesPromise.done(function () { + if(_resultsView) { + _resultsView.open(); + } + }).fail(function () { + if(_resultsView) { + _resultsView.close(); + } + editor.displayErrorMessageAtCursor(errorMsg); + result.reject(); + }); + } else { + if(_resultsView) { + _resultsView.close(); + } + editor.displayErrorMessageAtCursor(errorMsg); + result.reject(); + } + + return result.promise(); + } + + /** + * @private + * Clears any previous search information, removing update listeners and clearing the model. + */ + function _clearSearch() { + searchModel.clear(); + } + + /** + * @public + * Closes the references panel + */ + function closeReferencesPanel() { + if (_resultsView) { + _resultsView.close(); + } + } + + function setMenuItemStateForLanguage(languageId) { + CommandManager.get(Commands.CMD_FIND_ALL_REFERENCES).setEnabled(false); + if (!languageId) { + var editor = EditorManager.getActiveEditor(); + if (editor) { + languageId = LanguageManager.getLanguageForPath(editor.document.file._path).getId(); + } + } + var enabledProviders = _providerRegistrationHandler.getProvidersForLanguageId(languageId), + referencesProvider; + + enabledProviders.some(function (item, index) { + if (item.provider.hasReferences()) { + referencesProvider = item.provider; + return true; + } + }); + if (referencesProvider) { + CommandManager.get(Commands.CMD_FIND_ALL_REFERENCES).setEnabled(true); + } + + } + + MainViewManager.on("currentFileChange", function (event, newFile, newPaneId, oldFile, oldPaneId) { + if (!newFile) { + CommandManager.get(Commands.CMD_FIND_ALL_REFERENCES).setEnabled(false); + return; + } + + var newFilePath = newFile.fullPath, + newLanguageId = LanguageManager.getLanguageForPath(newFilePath).getId(); + setMenuItemStateForLanguage(newLanguageId); + + DocumentManager.getDocumentForPath(newFilePath) + .done(function (newDoc) { + newDoc.on("languageChanged.reference-in-files", function () { + var changedLanguageId = LanguageManager.getLanguageForPath(newDoc.file.fullPath).getId(); + setMenuItemStateForLanguage(changedLanguageId); + }); + }); + + if (!oldFile) { + return; + } + + var oldFilePath = oldFile.fullPath; + DocumentManager.getDocumentForPath(oldFilePath) + .done(function (oldDoc) { + oldDoc.off("languageChanged.reference-in-files"); + }); + }); + + AppInit.htmlReady(function () { + _resultsView = new SearchResultsView( + searchModel, + "reference-in-files-results", + "reference-in-files.results", + "reference" + ); + if(_resultsView) { + _resultsView + .on("close", function () { + _clearSearch(); + }) + .on("getNextPage", function () { + if (searchModel.hasResults()) { + _resultsView.showNextPage(); + } + }) + .on("getLastPage", function () { + if (searchModel.hasResults()) { + _resultsView.showLastPage(); + } + }); + } + }); + + // Initialize: register listeners + ProjectManager.on("beforeProjectClose", function () { if (_resultsView) { _resultsView.close(); } }); + + CommandManager.register(Strings.FIND_ALL_REFERENCES, Commands.CMD_FIND_ALL_REFERENCES, _openReferencesPanel); + CommandManager.get(Commands.CMD_FIND_ALL_REFERENCES).setEnabled(false); + + exports.registerFindReferencesProvider = registerFindReferencesProvider; + exports.removeFindReferencesProvider = removeFindReferencesProvider; + exports.setMenuItemStateForLanguage = setMenuItemStateForLanguage; + exports.closeReferencesPanel = closeReferencesPanel; +}); diff --git a/src/features/JumpToDefManager.js b/src/features/JumpToDefManager.js new file mode 100644 index 00000000000..570c16fe3a2 --- /dev/null +++ b/src/features/JumpToDefManager.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +define(function (require, exports, module) { + "use strict"; + + var Commands = require("command/Commands"), + Strings = require("strings"), + AppInit = require("utils/AppInit"), + CommandManager = require("command/CommandManager"), + EditorManager = require("editor/EditorManager"), + ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler; + + var _providerRegistrationHandler = new ProviderRegistrationHandler(), + registerJumpToDefProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler), + removeJumpToDefProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + + + /** + * Asynchronously asks providers to handle jump-to-definition. + * @return {!Promise} Resolved when the provider signals that it's done; rejected if no + * provider responded or the provider that responded failed. + */ + function _doJumpToDef() { + var request = null, + result = new $.Deferred(), + jumpToDefProvider = null, + editor = EditorManager.getActiveEditor(); + + if (editor) { + // Find a suitable provider, if any + var language = editor.getLanguageForSelection(), + enabledProviders = _providerRegistrationHandler.getProvidersForLanguageId(language.getId()); + + + enabledProviders.some(function (item, index) { + if (item.provider.canJumpToDef(editor)) { + jumpToDefProvider = item.provider; + return true; + } + }); + + if (jumpToDefProvider) { + request = jumpToDefProvider.doJumpToDef(editor); + + if (request) { + request.done(function () { + result.resolve(); + }).fail(function () { + result.reject(); + }); + } else { + result.reject(); + } + } else { + result.reject(); + } + } else { + result.reject(); + } + + return result.promise(); + } + + CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, Commands.NAVIGATE_JUMPTO_DEFINITION, _doJumpToDef); + + exports.registerJumpToDefProvider = registerJumpToDefProvider; + exports.removeJumpToDefProvider = removeJumpToDefProvider; +}); diff --git a/src/features/ParameterHintsManager.js b/src/features/ParameterHintsManager.js new file mode 100644 index 00000000000..adf2b5c5352 --- /dev/null +++ b/src/features/ParameterHintsManager.js @@ -0,0 +1,416 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +/* eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + "use strict"; + + var _ = require("thirdparty/lodash"); + + var Commands = require("command/Commands"), + AppInit = require("utils/AppInit"), + CommandManager = require("command/CommandManager"), + EditorManager = require("editor/EditorManager"), + Menus = require("command/Menus"), + KeyEvent = require("utils/KeyEvent"), + Strings = require("strings"), + ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler; + + + /** @const {string} Show Function Hint command ID */ + var SHOW_PARAMETER_HINT_CMD_ID = "showParameterHint", // string must MATCH string in native code (brackets_extensions) + hintContainerHTML = require("text!htmlContent/parameter-hint-template.html"), + KeyboardPrefs = { + "showParameterHint": [ + { + "key": "Ctrl-Shift-Space" + }, + { + "key": "Ctrl-Shift-Space", + "platform": "mac" + } + ] + }; + + var $hintContainer, // function hint container + $hintContent, // function hint content holder + hintState = {}, + lastChar = null, + sessionEditor = null, + keyDownEditor = null; + + // Constants + var POINTER_TOP_OFFSET = 4, // Size of margin + border of hint. + POSITION_BELOW_OFFSET = 4; // Amount to adjust to top position when the preview bubble is below the text + + // keep jslint from complaining about handleCursorActivity being used before + // it was defined. + var handleCursorActivity; + + var _providerRegistrationHandler = new ProviderRegistrationHandler(), + registerHintProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler), + removeHintProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + + /** + * Position a function hint. + * + * @param {number} xpos + * @param {number} ypos + * @param {number} ybot + */ + function positionHint(xpos, ypos, ybot) { + var hintWidth = $hintContainer.width(), + hintHeight = $hintContainer.height(), + top = ypos - hintHeight - POINTER_TOP_OFFSET, + left = xpos, + $editorHolder = $("#editor-holder"), + editorLeft; + + if ($editorHolder.offset() === undefined) { + // this happens in jasmine tests that run + // without a windowed document. + return; + } + + editorLeft = $editorHolder.offset().left; + left = Math.max(left, editorLeft); + left = Math.min(left, editorLeft + $editorHolder.width() - hintWidth); + + if (top < 0) { + $hintContainer.removeClass("preview-bubble-above"); + $hintContainer.addClass("preview-bubble-below"); + top = ybot + POSITION_BELOW_OFFSET; + $hintContainer.offset({ + left: left, + top: top + }); + } else { + $hintContainer.removeClass("preview-bubble-below"); + $hintContainer.addClass("preview-bubble-above"); + $hintContainer.offset({ + left: left, + top: top - POINTER_TOP_OFFSET + }); + } + } + + /** + * Format the given parameter array. Handles separators between + * parameters, syntax for optional parameters, and the order of the + * parameter type and parameter name. + * + * @param {!Array.<{name: string, type: string, isOptional: boolean}>} params - + * array of parameter descriptors + * @param {function(string)=} appendSeparators - callback function to append separators. + * The separator is passed to the callback. + * @param {function(string, number)=} appendParameter - callback function to append parameter. + * The formatted parameter type and name is passed to the callback along with the + * current index of the parameter. + * @param {boolean=} typesOnly - only show parameter types. The + * default behavior is to include both parameter names and types. + * @return {string} - formatted parameter hint + */ + function _formatParameterHint(params, appendSeparators, appendParameter, typesOnly) { + var result = "", + pendingOptional = false; + + appendParameter("(", "", -1); + params.forEach(function (value, i) { + var param = value.label || value.type, + documentation = value.documentation, + separators = ""; + + if (value.isOptional) { + // if an optional param is following by an optional parameter, then + // terminate the bracket. Otherwise enclose a required parameter + // in the same bracket. + if (pendingOptional) { + separators += "]"; + } + + pendingOptional = true; + } + + if (i > 0) { + separators += ", "; + } + + if (value.isOptional) { + separators += "["; + } + + if (appendSeparators) { + appendSeparators(separators); + } + + result += separators; + + if (!typesOnly && value.name) { + param += " " + value.name; + } + + if (appendParameter) { + appendParameter(param, documentation, i); + } + + result += param; + + }); + + if (pendingOptional) { + if (appendSeparators) { + appendSeparators("]"); + } + + result += "]"; + } + appendParameter(")", "", -1); + + return result; + } + + /** + * Bold the parameter at the caret. + * + * @param {{inFunctionCall: boolean, functionCallPos: {line: number, ch: number}}} functionInfo - + * tells if the caret is in a function call and the position + * of the function call. + */ + function formatHint(hints) { + $hintContent.empty(); + $hintContent.addClass("brackets-hints"); + + function appendSeparators(separators) { + $hintContent.append(separators); + } + + function appendParameter(param, documentation, index) { + if (hints.currentIndex === index) { + $hintContent.append($("") + .append(_.escape(param)) + .addClass("current-parameter")); + } else { + $hintContent.append($("") + .append(_.escape(param)) + .addClass("parameter")); + } + } + + if (hints.parameters.length > 0) { + _formatParameterHint(hints.parameters, appendSeparators, appendParameter); + } else { + $hintContent.append(_.escape(Strings.NO_ARGUMENTS)); + } + } + + /** + * Dismiss the function hint. + * + */ + function dismissHint(editor) { + if (hintState.visible) { + $hintContainer.hide(); + $hintContent.empty(); + hintState = {}; + + if (editor) { + editor.off("cursorActivity.ParameterHinting", handleCursorActivity); + sessionEditor = null; + } else if (sessionEditor) { + sessionEditor.off("cursorActivity.ParameterHinting", handleCursorActivity); + sessionEditor = null; + } + } + } + + /** + * Pop up a function hint on the line above the caret position. + * + * @param {object=} editor - current Active Editor + * @param {boolean} True if hints are invoked through cursor activity. + * @return {jQuery.Promise} - The promise will not complete until the + * hint has completed. Returns null, if the function hint is already + * displayed or there is no function hint at the cursor. + * + */ + function popUpHint(editor, explicit, onCursorActivity) { + var request = null; + var $deferredPopUp = $.Deferred(); + var sessionProvider = null; + + dismissHint(editor); + // Find a suitable provider, if any + var language = editor.getLanguageForSelection(), + enabledProviders = _providerRegistrationHandler.getProvidersForLanguageId(language.getId()); + + enabledProviders.some(function (item, index) { + if (item.provider.hasParameterHints(editor, lastChar)) { + sessionProvider = item.provider; + return true; + } + }); + + if (sessionProvider) { + request = sessionProvider.getParameterHints(explicit, onCursorActivity); + } + + if (request) { + request.done(function (parameterHint) { + var cm = editor._codeMirror, + pos = parameterHint.functionCallPos || editor.getCursorPos(); + + pos = cm.charCoords(pos); + formatHint(parameterHint); + + $hintContainer.show(); + positionHint(pos.left, pos.top, pos.bottom); + hintState.visible = true; + + sessionEditor = editor; + editor.on("cursorActivity.ParameterHinting", handleCursorActivity); + $deferredPopUp.resolveWith(null); + }).fail(function () { + hintState = {}; + }); + } + + return $deferredPopUp; + } + + /** + * Show the parameter the cursor is on in bold when the cursor moves. + * Dismiss the pop up when the cursor moves off the function. + */ + handleCursorActivity = function (event, editor) { + if (editor) { + popUpHint(editor, false, true); + } else { + dismissHint(); + } + }; + + /** + * Install function hint listeners. + * + * @param {Editor} editor - editor context on which to listen for + * changes + */ + function installListeners(editor) { + editor.on("keydown.ParameterHinting", function (event, editor, domEvent) { + if (domEvent.keyCode === KeyEvent.DOM_VK_ESCAPE) { + dismissHint(editor); + } + }).on("scroll.ParameterHinting", function () { + dismissHint(editor); + }) + .on("editorChange.ParameterHinting", _handleChange) + .on("keypress.ParameterHinting", _handleKeypressEvent); + } + + /** + * Clean up after installListeners() + * @param {!Editor} editor + */ + function uninstallListeners(editor) { + editor.off(".ParameterHinting"); + } + + function _handleKeypressEvent(jqEvent, editor, event) { + keyDownEditor = editor; + // Last inserted character, used later by handleChange + lastChar = String.fromCharCode(event.charCode); + } + + /** + * Start a new implicit hinting session, or update the existing hint list. + * Called by the editor after handleKeyEvent, which is responsible for setting + * the lastChar. + * + * @param {Event} event + * @param {Editor} editor + * @param {{from: Pos, to: Pos, text: Array, origin: string}} changeList + */ + function _handleChange(event, editor, changeList) { + if (lastChar && (lastChar === '(' || lastChar === ',') && editor === keyDownEditor) { + keyDownEditor = null; + popUpHint(editor); + } + } + + function activeEditorChangeHandler(event, current, previous) { + + if (previous) { + //Removing all old Handlers + previous.document + .off("languageChanged.ParameterHinting"); + uninstallListeners(previous); + } + + if (current) { + current.document + .on("languageChanged.ParameterHinting", function () { + // If current doc's language changed, reset our state by treating it as if the user switched to a + // different document altogether + uninstallListeners(current); + installListeners(current); + }); + installListeners(current); + } + } + + /** + * Show a parameter hint in its own pop-up. + * + */ + function handleShowParameterHint() { + var editor = EditorManager.getActiveEditor(); + // Pop up function hint + popUpHint(editor, true, false); + } + + AppInit.appReady(function () { + CommandManager.register(Strings.CMD_SHOW_PARAMETER_HINT, SHOW_PARAMETER_HINT_CMD_ID, handleShowParameterHint); + + // Add the menu items + var menu = Menus.getMenu(Menus.AppMenuBar.EDIT_MENU); + if (menu) { + menu.addMenuItem(SHOW_PARAMETER_HINT_CMD_ID, KeyboardPrefs.showParameterHint, Menus.AFTER, Commands.SHOW_CODE_HINTS); + } + // Create the function hint container + $hintContainer = $(hintContainerHTML).appendTo($("body")); + $hintContent = $hintContainer.find(".function-hint-content-new"); + activeEditorChangeHandler(null, EditorManager.getActiveEditor(), null); + + EditorManager.on("activeEditorChange", activeEditorChangeHandler); + + CommandManager.on("beforeExecuteCommand", function (event, commandId) { + if (commandId !== SHOW_PARAMETER_HINT_CMD_ID && + commandId !== Commands.SHOW_CODE_HINTS) { + dismissHint(); + } + }); + }); + + exports.registerHintProvider = registerHintProvider; + exports.removeHintProvider = removeHintProvider; +}); diff --git a/src/features/PriorityBasedRegistration.js b/src/features/PriorityBasedRegistration.js new file mode 100644 index 00000000000..1ad6214d48e --- /dev/null +++ b/src/features/PriorityBasedRegistration.js @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019 - present Adobe. 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. + * + */ + +/* eslint-disable indent */ +define(function (require, exports, module) { + "use strict"; + + var PreferencesManager = require("preferences/PreferencesManager"); + + /** + * Comparator to sort providers from high to low priority + */ + function _providerSort(a, b) { + return b.priority - a.priority; + } + + + function RegistrationHandler() { + this._providers = { + "all": [] + }; + } + + /** + * The method by which a Provider registers its willingness to + * providing tooling feature for editors in a given language. + * + * @param {!Provider} provider + * The provider to be registered, described below. + * + * @param {!Array.} languageIds + * The set of language ids for which the provider is capable of + * providing tooling feature. If the special language id name "all" is included then + * the provider may be called for any language. + * + * @param {?number} priority + * Used to break ties among providers for a particular language. + * Providers with a higher number will be asked for tooling before those + * with a lower priority value. Defaults to zero. + */ + RegistrationHandler.prototype.registerProvider = function (providerInfo, languageIds, priority) { + var providerObj = { + provider: providerInfo, + priority: priority || 0 + }, + self = this; + + if (languageIds.indexOf("all") !== -1) { + // Ignore anything else in languageIds and just register for every language. This includes + // the special "all" language since its key is in the hintProviders map from the beginning. + var languageId; + for (languageId in self._providers) { + if (self._providers.hasOwnProperty(languageId)) { + self._providers[languageId].push(providerObj); + self._providers[languageId].sort(_providerSort); + } + } + } else { + languageIds.forEach(function (languageId) { + if (!self._providers[languageId]) { + // Initialize provider list with any existing all-language providers + self._providers[languageId] = Array.prototype.concat(self._providers.all); + } + self._providers[languageId].push(providerObj); + self._providers[languageId].sort(_providerSort); + }); + } + }; + + /** + * Remove a code hint provider + * @param {!CodeHintProvider} provider Code hint provider to remove + * @param {(string|Array.)=} targetLanguageId Optional set of + * language IDs for languages to remove the provider for. Defaults + * to all languages. + */ + RegistrationHandler.prototype.removeProvider = function (provider, targetLanguageId) { + var index, + providers, + targetLanguageIdArr, + self = this; + + if (Array.isArray(targetLanguageId)) { + targetLanguageIdArr = targetLanguageId; + } else if (targetLanguageId) { + targetLanguageIdArr = [targetLanguageId]; + } else { + targetLanguageIdArr = Object.keys(self._providers); + } + + targetLanguageIdArr.forEach(function (languageId) { + providers = self._providers[languageId]; + + for (index = 0; index < providers.length; index++) { + if (providers[index].provider === provider) { + providers.splice(index, 1); + break; + } + } + }); + }; + + + RegistrationHandler.prototype.getProvidersForLanguageId = function (languageId) { + var providers = this._providers[languageId] || this._providers.all; + + // Exclude providers that are explicitly disabled in the preferences. + // All providers that do not have their constructor + // names listed in the preferences are enabled by default. + return providers.filter(function (provider) { + var prefKey = "tooling." + provider.provider.constructor.name; + return PreferencesManager.get(prefKey) !== false; + }); + }; + + + exports.RegistrationHandler = RegistrationHandler; +}); diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 308c389cd62..284c839b44e 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -59,6 +59,11 @@ define(function (require, exports, module) { */ var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024; + /** + * @const {List} list of File Extensions which will be opened in external Application + */ + var extListToBeOpenedInExtApp = []; + /** * Asynchronously reads a file as UTF-8 encoded text. @@ -526,6 +531,28 @@ define(function (require, exports, module) { return pathArray.join("/"); } + /** + * @param {string} ext extension string a file + * @return {string} returns true If file to be opened in External Application. + * + */ + function shouldOpenInExternalApplication(ext) { + return extListToBeOpenedInExtApp.includes(ext); + } + + /** + * @param {string} ext File Extensions to be added in External App List + * + */ + function addExtensionToExternalAppList(ext) { + + if(Array.isArray(ext)) { + extListToBeOpenedInExtApp = ext; + } else if (typeof ext === 'string'){ + extListToBeOpenedInExtApp.push(ext); + } + } + // Asynchronously load DocumentCommandHandlers // This avoids a temporary circular dependency created // by relocating showFileOpenError() until deprecation is over @@ -568,4 +595,6 @@ define(function (require, exports, module) { exports.comparePaths = comparePaths; exports.MAX_FILE_SIZE = MAX_FILE_SIZE; exports.encodeFilePath = encodeFilePath; + exports.shouldOpenInExternalApplication = shouldOpenInExternalApplication; + exports.addExtensionToExternalAppList = addExtensionToExternalAppList; }); diff --git a/src/filesystem/FileIndex.js b/src/filesystem/FileIndex.js index 6097908bc2e..fcc73068f5a 100644 --- a/src/filesystem/FileIndex.js +++ b/src/filesystem/FileIndex.js @@ -29,6 +29,8 @@ define(function (require, exports, module) { "use strict"; + var FileUtils = require("file/FileUtils"); + /** * @constructor */ @@ -110,7 +112,9 @@ define(function (require, exports, module) { */ FileIndex.prototype.entryRenamed = function (oldPath, newPath, isDirectory) { var path, - renameMap = {}; + renameMap = {}, + oldParentPath = FileUtils.getParentPath(oldPath), + newParentPath = FileUtils.getParentPath(newPath); // Find all entries affected by the rename and put into a separate map. for (path in this._index) { @@ -138,6 +142,30 @@ define(function (require, exports, module) { item._setPath(renameMap[path]); } } + + + // If file path is changed, i.e the file is moved + // Remove the moved entry from old Directory and add it to new Directory + if (oldParentPath !== newParentPath) { + var oldDirectory = this._index[oldParentPath], + newDirectory = this._index[newParentPath], + renamedEntry; + + if (oldDirectory && oldDirectory._contents) { + oldDirectory._contents = oldDirectory._contents.filter(function(entry) { + if (entry.fullPath === newPath) { + renamedEntry = entry; + return false; + } + return true; + }); + } + + if (newDirectory && newDirectory._contents && renamedEntry) { + renamedEntry._setPath(newPath); + newDirectory._contents.push(renamedEntry); + } + } }; /** diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 8a3227dec5f..c9bf25b9521 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -94,7 +94,63 @@ define(function (require, exports, module) { FileIndex = require("filesystem/FileIndex"), FileSystemError = require("filesystem/FileSystemError"), WatchedRoot = require("filesystem/WatchedRoot"), - EventDispatcher = require("utils/EventDispatcher"); + EventDispatcher = require("utils/EventDispatcher"), + PathUtils = require("thirdparty/path-utils/path-utils"), + _ = require("thirdparty/lodash"); + + + // Collection of registered protocol adapters + var _fileProtocolPlugins = {}; + + /** + * Typical signature of a file protocol adapter. + * @typedef {Object} FileProtocol~Adapter + * @property {Number} priority - Indicates the priority. + * @property {Object} fileImpl - Handle for the custom file implementation prototype. + * @property {function} canRead - To check if this impl can read a file for a given path. + */ + + /** + * FileSystem hook to register file protocol adapter + * @param {string} protocol ex: "https:"|"http:"|"ftp:"|"file:" + * @param {...FileProtocol~Adapter} adapter wrapper over file implementation + */ + function registerProtocolAdapter(protocol, adapter) { + var adapters; + if (protocol) { + adapters = _fileProtocolPlugins[protocol] || []; + adapters.push(adapter); + + // We will keep a sorted adapter list on 'priority' + // If priority is not provided a default of '0' is assumed + adapters.sort(function (a, b) { + return (b.priority || 0) - (a.priority || 0); + }); + + _fileProtocolPlugins[protocol] = adapters; + } + } + + /** + * @param {string} protocol ex: "https:"|"http:"|"ftp:"|"file:" + * @param {string} filePath fullPath of the file + * @return adapter adapter wrapper over file implementation + */ + function _getProtocolAdapter(protocol, filePath) { + var protocolAdapters = _fileProtocolPlugins[protocol] || [], + selectedAdapter; + + // Find the fisrt compatible adapter having highest priority + _.forEach(protocolAdapters, function (adapter) { + if (adapter.canRead && adapter.canRead(filePath)) { + selectedAdapter = adapter; + // Break at first compatible adapter + return false; + } + }); + + return selectedAdapter; + } /** * The FileSystem is not usable until init() signals its callback. @@ -572,7 +628,14 @@ define(function (require, exports, module) { * @return {File} The File object. This file may not yet exist on disk. */ FileSystem.prototype.getFileForPath = function (path) { - return this._getEntryForPath(File, path); + var protocol = PathUtils.parseUrl(path).protocol, + protocolAdapter = _getProtocolAdapter(protocol); + + if (protocolAdapter && protocolAdapter.fileImpl) { + return new protocolAdapter.fileImpl(protocol, path, this); + } else { + return this._getEntryForPath(File, path); + } }; /** @@ -1005,6 +1068,7 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; + exports.registerProtocolAdapter = registerProtocolAdapter; // For testing only exports._getActiveChangeCount = _wrap(FileSystem.prototype._getActiveChangeCount); diff --git a/src/htmlContent/image-view.html b/src/htmlContent/image-view.html index 829a4cd2904..553e01b7472 100644 --- a/src/htmlContent/image-view.html +++ b/src/htmlContent/image-view.html @@ -6,7 +6,7 @@
    - +
    @@ -23,4 +23,4 @@
    - \ No newline at end of file + diff --git a/src/htmlContent/infobar-template.html b/src/htmlContent/infobar-template.html new file mode 100644 index 00000000000..ab73c367b52 --- /dev/null +++ b/src/htmlContent/infobar-template.html @@ -0,0 +1,13 @@ +
    +
    + +
    +
    +

    {{title}}  {{{description}}}

    +
    + {{^buttons}} +
    + +
    + {{/buttons}} +
    \ No newline at end of file diff --git a/src/htmlContent/parameter-hint-template.html b/src/htmlContent/parameter-hint-template.html new file mode 100644 index 00000000000..ffdd53d620b --- /dev/null +++ b/src/htmlContent/parameter-hint-template.html @@ -0,0 +1,4 @@ +
    +
    +
    +
    diff --git a/src/htmlContent/update-dialog.html b/src/htmlContent/update-dialog.html index 0355a413445..8d51905c598 100644 --- a/src/htmlContent/update-dialog.html +++ b/src/htmlContent/update-dialog.html @@ -1,9 +1,9 @@