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/.travis.yml b/.travis.yml index 464408502fc..b2803e1d928 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: node_js sudo: false # use container-based Travis infrastructure node_js: - "6" +before_install: + - phpenv global 7.0 #switch to php7, since that's what php-Tooling extension requires before_script: - npm install -g grunt-cli - npm install -g jasmine-node diff --git a/Gruntfile.js b/Gruntfile.js index fe7e2d0a1d0..0a276991b8c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -82,6 +82,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/**/*', diff --git a/package.json b/package.json index 67a939326f1..39a007375b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Brackets", - "version": "1.13.0-0", - "apiVersion": "1.13.0", + "version": "1.14.0-0", + "apiVersion": "1.14.0", "homepage": "http://brackets.io", "issues": { "url": "http://github.com/adobe/brackets/issues" @@ -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/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 bebcd37a9a2..76137056939 100644 --- a/src/brackets.config.dev.json +++ b/src/brackets.config.dev.json @@ -4,5 +4,6 @@ "serviceKey" : "brackets-service", "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 9fdc775054b..c3411b31a28 100644 --- a/src/brackets.config.dist.json +++ b/src/brackets.config.dist.json @@ -4,5 +4,6 @@ "serviceKey" : "brackets-service", "environment" : "production", "update_info_url" : "https://getupdates.brackets.io/getupdates/", + "notification_info_url" : "https://getupdates.brackets.io/getnotifications?locale=", "buildtype" : "production" } diff --git a/src/brackets.config.prerelease.json b/src/brackets.config.prerelease.json index ee527136d4b..ac8e9de01e9 100644 --- a/src/brackets.config.prerelease.json +++ b/src/brackets.config.prerelease.json @@ -4,5 +4,6 @@ "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..27c543879c9 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 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 26d7b240ddb..016a404abb7 100644 --- a/src/config.json +++ b/src/config.json @@ -26,8 +26,8 @@ "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.14.0-0", + "apiVersion": "1.14.0", "homepage": "http://brackets.io", "issues": { "url": "http://github.com/adobe/brackets/issues" @@ -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": [ diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 28ea9a9dfb0..ed875376d9f 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -689,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(); } @@ -788,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) { @@ -967,6 +977,7 @@ define(function (require, exports, module) { } else { openNewFile(); } + HealthLogger.fileSaved(doc); }) .fail(function (error) { _showSaveFileError(error, path) @@ -1186,6 +1197,7 @@ define(function (require, exports, module) { function doClose(file) { if (!promptOnly) { MainViewManager._close(paneId, file); + HealthLogger.fileClosed(file); } } 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/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 c3a53857fae..7d27e926424 100644 --- a/src/editor/EditorStatusBar.js +++ b/src/editor/EditorStatusBar.js @@ -465,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/extensions/default/AutoUpdate/MessageIds.js b/src/extensions/default/AutoUpdate/MessageIds.js index b969aab6150..464ddf48836 100644 --- a/src/extensions/default/AutoUpdate/MessageIds.js +++ b/src/extensions/default/AutoUpdate/MessageIds.js @@ -38,6 +38,6 @@ define(function (require, exports, module) { exports.NOTIFY_INITIALIZATION_COMPLETE = "brackets.notifyinitializationComplete"; exports.NOTIFY_VALIDATION_STATUS = "brackets.notifyvalidationStatus"; exports.NOTIFY_INSTALLATION_STATUS = "brackets.notifyInstallationStatus"; - exports.SET_UPDATE_IN_PROGRESS_STATE = "brackets.setAutoUpdateInProgress"; + exports.NODE_DOMAIN_INITIALIZED = "brackets.nodeDomainInitialized"; exports.REGISTER_BRACKETS_FUNCTIONS = "brackets.registerBracketsFunctions"; }); diff --git a/src/extensions/default/AutoUpdate/main.js b/src/extensions/default/AutoUpdate/main.js index 1a36c486178..5fd13a0d0c4 100644 --- a/src/extensions/default/AutoUpdate/main.js +++ b/src/extensions/default/AutoUpdate/main.js @@ -223,6 +223,7 @@ define(function (require, exports, module) { if (downloadCompleted && updateInitiatedInPrevSession) { var isNewVersion = checkIfVersionUpdated(); + updateJsonHandler.reset(); if (isNewVersion) { // We get here if the update was successful UpdateInfoBar.showUpdateBar({ @@ -280,19 +281,14 @@ define(function (require, exports, module) { /** * 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() { - checkIfAnotherSessionInProgress() - .done(function (inProgress) { - if (!inProgress) { - checkUpdateStatus(); - } - }) - .fail(function () { - checkUpdateStatus(); - }); + result.resolve(); }) .fail(function (code) { var logMsg; @@ -311,7 +307,9 @@ define(function (require, exports, module) { break; } console.log(logMsg); + result.reject(); }); + return result.promise(); } @@ -321,39 +319,16 @@ define(function (require, exports, module) { */ function setupAutoUpdate() { updateJsonHandler = new StateHandler(updateJsonPath); + updateDomain.on('data', receiveMessageFromNode); updateDomain.exec('initNode', { messageIds: MessageIds, updateDir: updateDir, requester: domainID }); - - updateDomain.on('data', receiveMessageFromNode); - initState(); } - /** - * Generates the extension for installer file, based on platform - * @returns {string} - OS - current OS } - */ - function getPlatformInfo() { - var OS = ""; - - if (/Windows|Win32|WOW64|Win64/.test(window.navigator.userAgent)) { - OS = "WIN"; - } else if (/Mac/.test(window.navigator.userAgent)) { - OS = "OSX"; - } else if (/Linux|X11/.test(window.navigator.userAgent)) { - OS = "LINUX32"; - if (/x86_64/.test(window.navigator.appVersion + window.navigator.userAgent)) { - OS = "LINUX64"; - } - } - - return OS; - } - /** * Initializes the state for AutoUpdate process * @returns {$.Deferred} - a jquery promise, @@ -470,7 +445,7 @@ define(function (require, exports, module) { console.warn("AutoUpdate : updates information not available."); return; } - var OS = getPlatformInfo(), + var OS = brackets.getPlatformInfo(), checksum, downloadURL, installerName, @@ -490,12 +465,12 @@ define(function (require, exports, module) { } else { // Update not present for current platform - return; + return false; } if (!checksum || !downloadURL || !installerName) { console.warn("AutoUpdate : asset information incorrect for the update"); - return; + return false; } var updateParams = { @@ -508,6 +483,9 @@ define(function (require, exports, module) { //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; } @@ -594,11 +572,17 @@ define(function (require, exports, module) { /** * Enables/disables the state of "Auto Update In Progress" in UpdateHandler.json */ - function setAutoUpdateInProgressFlag(flag) { - updateJsonHandler.parse() - .done(function() { - setUpdateStateInJSON("autoUpdateInProgress", flag); - }); + function nodeDomainInitialized(reset) { + initState() + .done(function () { + var inProgress = updateJsonHandler.get(updateProgressKey); + if (inProgress && reset) { + setUpdateStateInJSON(updateProgressKey, !reset) + .always(checkUpdateStatus); + } else if (!inProgress) { + checkUpdateStatus(); + } + }); } @@ -636,7 +620,6 @@ define(function (require, exports, module) { enableCheckForUpdateEntry(true); console.error(message); - setUpdateStateInJSON("autoUpdateInProgress", false); } /** @@ -1124,7 +1107,7 @@ define(function (require, exports, module) { ProjectManager.on("beforeProjectClose beforeAppClose", _handleAppClose); } - functionMap["brackets.setAutoUpdateInProgress"] = setAutoUpdateInProgressFlag; + 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 index 9e635d8f1b1..2f63ee3d7e7 100644 --- a/src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js +++ b/src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js @@ -419,6 +419,7 @@ * requester : ID of the current requester domain} */ function initNode(initObj) { + var resetUpdateProgres = false; if (!isNodeDomainInitialized) { MessageIds = initObj.messageIds; updateDir = path.resolve(initObj.updateDir); @@ -426,8 +427,9 @@ installStatusFilePath = path.resolve(updateDir, installStatusFile); registerNodeFunctions(); isNodeDomainInitialized = true; - postMessageToBrackets(MessageIds.SET_UPDATE_IN_PROGRESS_STATE, initObj.requester.toString(), false); + resetUpdateProgres = true; } + postMessageToBrackets(MessageIds.NODE_DOMAIN_INITIALIZED, initObj.requester.toString(), resetUpdateProgres); requesters[initObj.requester.toString()] = true; postMessageToBrackets(MessageIds.REGISTER_BRACKETS_FUNCTIONS, initObj.requester.toString()); } diff --git a/src/extensions/default/CSSCodeHints/CSSProperties.json b/src/extensions/default/CSSCodeHints/CSSProperties.json index 7b80dbca158..d9bd8370feb 100644 --- a/src/extensions/default/CSSCodeHints/CSSProperties.json +++ b/src/extensions/default/CSSCodeHints/CSSProperties.json @@ -197,7 +197,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 +209,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 a452a4e5471..e6a72b5a181 100644 --- a/src/extensions/default/HealthData/HealthDataManager.js +++ b/src/extensions/default/HealthData/HealthDataManager.js @@ -308,22 +308,16 @@ define(function (require, exports, module) { isHDTracking = prefs.get("healthDataTracking"), isEventDataAlreadySent; - var options = { - location: { - scope: "default" - } - }; - if (isHDTracking) { - isEventDataAlreadySent = PreferencesManager.getViewState(Eventparams.eventName); - PreferencesManager.setViewState(Eventparams.eventName, 1, options); + isEventDataAlreadySent = HealthLogger.analyticsEventMap.get(Eventparams.eventName); + HealthLogger.analyticsEventMap.set(Eventparams.eventName, true); if (!isEventDataAlreadySent || forceSend) { sendAnalyticsDataToServer(Eventparams) .done(function () { - PreferencesManager.setViewState(Eventparams.eventName, 1, options); + HealthLogger.analyticsEventMap.set(Eventparams.eventName, true); result.resolve(); }).fail(function () { - PreferencesManager.setViewState(Eventparams.eventName, 0, options); + HealthLogger.analyticsEventMap.set(Eventparams.eventName, false); result.reject(); }); } else { @@ -336,6 +330,17 @@ define(function (require, exports, module) { 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") { 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..f51684006cb --- /dev/null +++ b/src/extensions/default/InAppNotifications/main.js @@ -0,0 +1,324 @@ +/* + * 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) { + var allExtensions = ExtensionManager.extensions, + allExtnsMatched = true, + userExtensionKeys = Object.keys(allExtensions).filter(function(k) { + return allExtensions[k].installInfo.locationType === 'user'; + }); + + if (!filters.extensions) { + allExtnsMatched = userExtensionKeys.size === 0; + } else if (filters.extensions.length === 0) { + allExtnsMatched = userExtensionKeys.length > 0; + } else { + 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/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/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/features/FindReferencesManager.js b/src/features/FindReferencesManager.js new file mode 100644 index 00000000000..50b8cdfe52e --- /dev/null +++ b/src/features/FindReferencesManager.js @@ -0,0 +1,210 @@ +/* + * 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(); + } + + 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; +}); 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/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/languageTools/BracketsToNodeInterface.js b/src/languageTools/BracketsToNodeInterface.js new file mode 100644 index 00000000000..71c12970218 --- /dev/null +++ b/src/languageTools/BracketsToNodeInterface.js @@ -0,0 +1,121 @@ +/* + * 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 no-invalid-this: 0*/ +define(function (require, exports, module) { + "use strict"; + + function BracketsToNodeInterface(domain) { + this.domain = domain; + this.bracketsFn = {}; + + this._registerDataEvent(); + } + + BracketsToNodeInterface.prototype._messageHandler = function (evt, params) { + var methodName = params.method, + self = this; + + function _getErrorString(err) { + if (typeof err === "string") { + return err; + } else if (err && err.name && err.name === "Error") { + return err.message; + } + return "Error in executing " + methodName; + + } + + function _sendResponse(response) { + var responseParams = { + requestId: params.requestId, + params: response + }; + self.domain.exec("response", responseParams); + } + + function _sendError(err) { + var responseParams = { + requestId: params.requestId, + error: _getErrorString(err) + }; + self.domain.exec("response", responseParams); + } + + if (self.bracketsFn[methodName]) { + var method = self.bracketsFn[methodName]; + try { + var response = method.call(null, params.params); + if (params.respond && params.requestId) { + if (response.promise) { + response.done(function (result) { + _sendResponse(result); + }).fail(function (err) { + _sendError(err); + }); + } else { + _sendResponse(response); + } + } + } catch (err) { + if (params.respond && params.requestId) { + _sendError(err); + } + } + } + + }; + + + BracketsToNodeInterface.prototype._registerDataEvent = function () { + this.domain.on("data", this._messageHandler.bind(this)); + }; + + BracketsToNodeInterface.prototype.createInterface = function (methodName, isAsync) { + var self = this; + return function (params) { + var execEvent = isAsync ? "asyncData" : "data"; + var callObject = { + method: methodName, + params: params + }; + return self.domain.exec(execEvent, callObject); + }; + }; + + BracketsToNodeInterface.prototype.registerMethod = function (methodName, methodHandle) { + if (methodName && methodHandle && + typeof methodName === "string" && typeof methodHandle === "function") { + this.bracketsFn[methodName] = methodHandle; + } + }; + + BracketsToNodeInterface.prototype.registerMethods = function (methodList) { + var self = this; + methodList.forEach(function (methodObj) { + self.registerMethod(methodObj.methodName, methodObj.methodHandle); + }); + }; + + exports.BracketsToNodeInterface = BracketsToNodeInterface; +}); diff --git a/src/languageTools/ClientLoader.js b/src/languageTools/ClientLoader.js new file mode 100644 index 00000000000..63d5c28784a --- /dev/null +++ b/src/languageTools/ClientLoader.js @@ -0,0 +1,179 @@ +/* + * 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 no-console: 0*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + "use strict"; + + var ToolingInfo = JSON.parse(require("text!languageTools/ToolingInfo.json")), + NodeDomain = require("utils/NodeDomain"), + FileUtils = require("file/FileUtils"), + EventDispatcher = require("utils/EventDispatcher"), + BracketsToNodeInterface = require("languageTools/BracketsToNodeInterface").BracketsToNodeInterface; + + EventDispatcher.makeEventDispatcher(exports); + //Register paths required for Language Client and also register default brackets capabilities. + var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(); + // The native directory path ends with either "test" or "src". + _bracketsPath = _bracketsPath.replace(/\/test$/, "/src"); // convert from "test" to "src" + + var _modulePath = FileUtils.getNativeModuleDirectoryPath(module), + _nodePath = "node/RegisterLanguageClientInfo", + _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), + clientInfoDomain = null, + clientInfoLoadedPromise = null, + //Clients that have to be loaded once the LanguageClient info is successfully loaded on the + //node side. + pendingClientsToBeLoaded = []; + + function syncPrefsWithDomain(languageToolsPrefs) { + if (clientInfoDomain) { + clientInfoDomain.exec("syncPreferences", languageToolsPrefs); + } + } + + function _createNodeDomain(domainName, domainPath) { + return new NodeDomain(domainName, domainPath); + } + + function loadLanguageClientDomain(clientName, domainPath) { + //generate a random hash name for the domain, this is the client id + var domainName = clientName, + result = $.Deferred(), + languageClientDomain = _createNodeDomain(domainName, domainPath); + + if (languageClientDomain) { + languageClientDomain.promise() + .done(function () { + console.log(domainPath + " domain successfully created"); + result.resolve(languageClientDomain); + }) + .fail(function (err) { + console.error(domainPath + " domain could not be created."); + result.reject(); + }); + } else { + console.error(domainPath + " domain could not be created."); + result.reject(); + } + + return result; + } + + function createNodeInterfaceForDomain(languageClientDomain) { + var nodeInterface = new BracketsToNodeInterface(languageClientDomain); + + return nodeInterface; + } + + function _clientLoader(clientName, clientFilePath, clientPromise) { + loadLanguageClientDomain(clientName, clientFilePath) + .then(function (languageClientDomain) { + var languageClientInterface = createNodeInterfaceForDomain(languageClientDomain); + + clientPromise.resolve({ + name: clientName, + interface: languageClientInterface + }); + }, clientPromise.reject); + } + + function initiateLanguageClient(clientName, clientFilePath) { + var result = $.Deferred(); + + //Only load clients after the LanguageClient Info has been initialized + if (!clientInfoLoadedPromise || clientInfoLoadedPromise.state() === "pending") { + var pendingClient = { + load: _clientLoader.bind(null, clientName, clientFilePath, result) + }; + pendingClientsToBeLoaded.push(pendingClient); + } else { + _clientLoader(clientName, clientFilePath, result); + } + + return result; + } + + /** + * This function passes Brackets's native directory path as well as the tooling commands + * required by the LanguageClient node module. This information is then maintained in memory + * in the node process server for succesfully loading and functioning of all language clients + * since it is a direct dependency. + */ + function sendLanguageClientInfo() { + //Init node with Information required by Language Client + clientInfoLoadedPromise = clientInfoDomain.exec("initialize", _bracketsPath, ToolingInfo); + + function logInitializationError() { + console.error("Failed to Initialize LanguageClient Module Information."); + } + + //Attach success and failure function for the clientInfoLoadedPromise + clientInfoLoadedPromise.then(function (success) { + if (!success) { + logInitializationError(); + return; + } + + if (Array.isArray(pendingClientsToBeLoaded)) { + pendingClientsToBeLoaded.forEach(function (pendingClient) { + pendingClient.load(); + }); + } else { + exports.trigger("languageClientModuleInitialized"); + } + pendingClientsToBeLoaded = null; + }, function () { + logInitializationError(); + }); + } + + /** + * This function starts a domain which initializes the LanguageClient node module + * required by the Language Server Protocol framework in Brackets. All the LSP clients + * can only be successfully initiated once this domain has been successfully loaded and + * the LanguageClient info initialized. Refer to sendLanguageClientInfo for more. + */ + function initDomainAndHandleNodeCrash() { + clientInfoDomain = new NodeDomain("LanguageClientInfo", _domainPath); + //Initialize LanguageClientInfo once the domain has successfully loaded. + clientInfoDomain.promise().done(function () { + sendLanguageClientInfo(); + //This is to handle the node failure. If the node process dies, we get an on close + //event on the websocket connection object. Brackets then spawns another process and + //restablishes the connection. Once the connection is restablished we send reinitialize + //the LanguageClient info. + clientInfoDomain.connection.on("close", function (event, reconnectedPromise) { + reconnectedPromise.done(sendLanguageClientInfo); + }); + }).fail(function (err) { + console.error("ClientInfo domain could not be loaded: ", err); + }); + } + initDomainAndHandleNodeCrash(); + + + exports.initiateLanguageClient = initiateLanguageClient; + exports.syncPrefsWithDomain = syncPrefsWithDomain; +}); diff --git a/src/languageTools/DefaultEventHandlers.js b/src/languageTools/DefaultEventHandlers.js new file mode 100644 index 00000000000..e544ae1b557 --- /dev/null +++ b/src/languageTools/DefaultEventHandlers.js @@ -0,0 +1,193 @@ +/* + * 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 no-console: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageManager = require("language/LanguageManager"), + ProjectManager = require("project/ProjectManager"), + PathConverters = require("languageTools/PathConverters"); + + function EventPropagationProvider(client) { + this.client = client; + this.previousProject = ""; + this.currentProject = ProjectManager.getProjectRoot(); + } + + EventPropagationProvider.prototype._sendDocumentOpenNotification = function (languageId, doc) { + if (!this.client) { + return; + } + + if (this.client._languages.includes(languageId)) { + this.client.notifyTextDocumentOpened({ + languageId: languageId, + filePath: (doc.file._path || doc.file.fullPath), + fileContent: doc.getText() + }); + } + }; + + EventPropagationProvider.prototype.handleActiveEditorChange = function (event, current, previous) { + var self = this; + + if (!this.client) { + return; + } + + if (previous) { + previous.document + .off("languageChanged.language-tools"); + var previousLanguageId = LanguageManager.getLanguageForPath(previous.document.file.fullPath).getId(); + if (this.client._languages.includes(previousLanguageId)) { + this.client.notifyTextDocumentClosed({ + filePath: (previous.document.file._path || previous.document.file.fullPath) + }); + } + } + if (current) { + var currentLanguageId = LanguageManager.getLanguageForPath(current.document.file.fullPath).getId(); + current.document + .on("languageChanged.language-tools", function () { + var languageId = LanguageManager.getLanguageForPath(current.document.file.fullPath).getId(); + self._sendDocumentOpenNotification(languageId, current.document); + }); + self._sendDocumentOpenNotification(currentLanguageId, current.document); + } + }; + + EventPropagationProvider.prototype.handleProjectOpen = function (event, directory) { + if (!this.client) { + return; + } + + this.currentProject = directory.fullPath; + + this.client.notifyProjectRootsChanged({ + foldersAdded: [this.currentProject], + foldersRemoved: [this.previousProject] + }); + }; + + EventPropagationProvider.prototype.handleProjectClose = function (event, directory) { + if (!this.client) { + return; + } + + this.previousProject = directory.fullPath; + }; + + EventPropagationProvider.prototype.handleDocumentDirty = function (event, doc) { + if (!this.client) { + return; + } + + if (!doc.isDirty) { + var docLanguageId = LanguageManager.getLanguageForPath(doc.file.fullPath).getId(); + if (this.client._languages.includes(docLanguageId)) { + this.client.notifyTextDocumentSave({ + filePath: (doc.file._path || doc.file.fullPath) + }); + } + } + }; + + EventPropagationProvider.prototype.handleDocumentChange = function (event, doc, changeList) { + if (!this.client) { + return; + } + + var docLanguageId = LanguageManager.getLanguageForPath(doc.file.fullPath).getId(); + if (this.client._languages.includes(docLanguageId)) { + this.client.notifyTextDocumentChanged({ + filePath: (doc.file._path || doc.file.fullPath), + fileContent: doc.getText() + }); + } + }; + + EventPropagationProvider.prototype.handleDocumentRename = function (event, oldName, newName) { + if (!this.client) { + return; + } + + var oldDocLanguageId = LanguageManager.getLanguageForPath(oldName).getId(); + if (this.client._languages.includes(oldDocLanguageId)) { + this.client.notifyTextDocumentClosed({ + filePath: oldName + }); + } + + var newDocLanguageId = LanguageManager.getLanguageForPath(newName).getId(); + if (this.client._languages.includes(newDocLanguageId)) { + this.client.notifyTextDocumentOpened({ + filePath: newName + }); + } + }; + + EventPropagationProvider.prototype.handleAppClose = function (event) { + //Also handles Reload with Extensions + if (!this.client) { + return; + } + + this.client.stop(); + }; + + function handleProjectFoldersRequest(event) { + var projectRoot = ProjectManager.getProjectRoot(), + workspaceFolders = [projectRoot]; + + workspaceFolders = PathConverters.convertToWorkspaceFolders(workspaceFolders); + + return $.Deferred().resolve(workspaceFolders); + }; + + EventPropagationProvider.prototype.registerClientForEditorEvent = function () { + if (this.client) { + var handleActiveEditorChange = this.handleActiveEditorChange.bind(this), + handleProjectOpen = this.handleProjectOpen.bind(this), + handleProjectClose = this.handleProjectClose.bind(this), + handleDocumentDirty = this.handleDocumentDirty.bind(this), + handleDocumentChange = this.handleDocumentChange.bind(this), + handleDocumentRename = this.handleDocumentRename.bind(this), + handleAppClose = this.handleAppClose.bind(this); + + this.client.addOnEditorChangeHandler(handleActiveEditorChange); + this.client.addOnProjectOpenHandler(handleProjectOpen); + this.client.addBeforeProjectCloseHandler(handleProjectClose); + this.client.addOnDocumentDirtyFlagChangeHandler(handleDocumentDirty); + this.client.addOnDocumentChangeHandler(handleDocumentChange); + this.client.addOnFileRenameHandler(handleDocumentRename); + this.client.addBeforeAppClose(handleAppClose); + this.client.onProjectFoldersRequest(handleProjectFoldersRequest); + } else { + console.log("No client provided for event propagation"); + } + }; + + exports.EventPropagationProvider = EventPropagationProvider; +}); diff --git a/src/languageTools/DefaultProviders.js b/src/languageTools/DefaultProviders.js new file mode 100644 index 00000000000..a75ea695de7 --- /dev/null +++ b/src/languageTools/DefaultProviders.js @@ -0,0 +1,516 @@ +/* + * 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 Map*/ +/* eslint-disable indent */ +/* eslint max-len: ["error", { "code": 200 }], no-invalid-this: 0*/ +define(function (require, exports, module) { + "use strict"; + + var _ = brackets.getModule("thirdparty/lodash"); + + var EditorManager = require('editor/EditorManager'), + DocumentManager = require('document/DocumentManager'), + ExtensionUtils = require("utils/ExtensionUtils"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + TokenUtils = require("utils/TokenUtils"), + StringMatch = require("utils/StringMatch"), + CodeInspection = require("language/CodeInspection"), + PathConverters = require("languageTools/PathConverters"), + matcher = new StringMatch.StringMatcher({ + preferPrefixMatches: true + }); + + ExtensionUtils.loadStyleSheet(module, "styles/default_provider_style.css"); + + function setClient(client) { + if (client) { + this.client = client; + } + } + + function CodeHintsProvider(client) { + this.client = client; + this.query = ""; + this.ignoreQuery = ["-", "->", ">", ":", "::", "(", "()", ")", "[", "[]", "]", "{", "{}", "}"]; + } + + CodeHintsProvider.prototype.setClient = setClient; + + function formatTypeDataForToken($hintObj, token) { + $hintObj.addClass('brackets-hints-with-type-details'); + if (token.detail) { + if (token.detail.trim() !== '?') { + if (token.detail.length < 30) { + $('' + token.detail.split('->').join(':').toString().trim() + '').appendTo($hintObj).addClass("brackets-hints-type-details"); + } + $('' + token.detail.split('->').join(':').toString().trim() + '').appendTo($hintObj).addClass("hint-description"); + } + } else { + if (token.keyword) { + $('keyword').appendTo($hintObj).addClass("brackets-hints-keyword"); + } + } + if (token.documentation) { + $hintObj.attr('title', token.documentation); + $('').text(token.documentation.trim()).appendTo($hintObj).addClass("hint-doc"); + } + } + + 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) { + if (!this.client) { + return false; + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.completionProvider) { + return false; + } + + return true; + }; + + CodeHintsProvider.prototype.getHints = function (implicitChar) { + if (!this.client) { + return null; + } + + var editor = EditorManager.getActiveEditor(), + pos = editor.getCursorPos(), + docPath = editor.document.file._path, + $deferredHints = $.Deferred(), + self = this; + + this.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, + 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); + formatTypeDataForToken($fHint, element); + hints.push($fHint); + }); + } + + $deferredHints.resolve({ + "hints": hints + }); + }).fail(function () { + $deferredHints.reject(); + }); + + return $deferredHints; + }; + + CodeHintsProvider.prototype.insertHint = function ($hint) { + var editor = EditorManager.getActiveEditor(), + cursor = editor.getCursorPos(), + token = $hint.data("token"), + txt = null, + query = this.query, + shouldIgnoreQuery = this.ignoreQuery.includes(query), + inclusion = shouldIgnoreQuery ? "" : query, + start = { + line: cursor.line, + ch: cursor.ch - inclusion.length + }, + end = { + line: cursor.line, + ch: cursor.ch + }; + + txt = token.label; + if (token.textEdit && token.textEdit.newText) { + txt = token.textEdit.newText; + start = { + line: token.textEdit.range.start.line, + ch: token.textEdit.range.start.character + }; + end = { + line: token.textEdit.range.end.line, + ch: token.textEdit.range.end.character + }; + } + + if (editor) { + editor.document.replaceRange(txt, start, end); + } + // Return false to indicate that another hinting session is not needed + return false; + }; + + function ParameterHintsProvider(client) { + this.client = client; + } + + ParameterHintsProvider.prototype.setClient = setClient; + + ParameterHintsProvider.prototype.hasParameterHints = function (editor, implicitChar) { + if (!this.client) { + return false; + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.signatureHelpProvider) { + return false; + } + + return true; + }; + + ParameterHintsProvider.prototype.getParameterHints = function () { + if (!this.client) { + return null; + } + + var editor = EditorManager.getActiveEditor(), + pos = editor.getCursorPos(), + docPath = editor.document.file._path, + $deferredHints = $.Deferred(); + + this.client.requestParameterHints({ + filePath: docPath, + cursorPos: pos + }).done(function (msgObj) { + let paramList = []; + let label; + let activeParameter; + if (msgObj) { + let res; + res = msgObj.signatures; + activeParameter = msgObj.activeParameter; + if (res && res.length) { + res.forEach(function (element) { + label = element.documentation; + let param = element.parameters; + param.forEach(ele => { + paramList.push({ + label: ele.label, + documentation: ele.documentation + }); + }); + }); + + $deferredHints.resolve({ + parameters: paramList, + currentIndex: activeParameter, + functionDocumentation: label + }); + } else { + $deferredHints.reject(); + } + } else { + $deferredHints.reject(); + } + }).fail(function () { + $deferredHints.reject(); + }); + + return $deferredHints; + }; + + /** + * Utility function to make the jump + * @param {Object} curPos - target postion for the cursor after the jump + */ + function setJumpPosition(curPos) { + EditorManager.getCurrentFullEditor().setCursorPos(curPos.line, curPos.ch, true); + } + + function JumpToDefProvider(client) { + this.client = client; + } + + JumpToDefProvider.prototype.setClient = setClient; + + JumpToDefProvider.prototype.canJumpToDef = function (editor, implicitChar) { + if (!this.client) { + return false; + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.definitionProvider) { + return false; + } + + return true; + }; + + /** + * Method to handle jump to definition feature. + */ + JumpToDefProvider.prototype.doJumpToDef = function () { + if (!this.client) { + return null; + } + + var editor = EditorManager.getFocusedEditor(), + pos = editor.getCursorPos(), + docPath = editor.document.file._path, + docPathUri = PathConverters.pathToUri(docPath), + $deferredHints = $.Deferred(); + + this.client.gotoDefinition({ + filePath: docPath, + cursorPos: pos + }).done(function (msgObj) { + //For Older servers + if (Array.isArray(msgObj)) { + msgObj = msgObj[msgObj.length - 1]; + } + + if (msgObj && msgObj.range) { + var docUri = msgObj.uri, + startCurPos = {}; + startCurPos.line = msgObj.range.start.line; + startCurPos.ch = msgObj.range.start.character; + + if (docUri !== docPathUri) { + let documentPath = PathConverters.uriToPath(docUri); + CommandManager.execute(Commands.FILE_OPEN, { + fullPath: documentPath + }) + .done(function () { + setJumpPosition(startCurPos); + $deferredHints.resolve(); + }); + } else { //definition is in current document + setJumpPosition(startCurPos); + $deferredHints.resolve(); + } + } + }).fail(function () { + $deferredHints.reject(); + }); + + return $deferredHints; + }; + + function LintingProvider() { + this._results = new Map(); + this._promiseMap = new Map(); + this._validateOnType = false; + } + + LintingProvider.prototype.setClient = setClient; + + LintingProvider.prototype.clearExistingResults = function (filePath) { + var filePathProvided = !!filePath; + + if (filePathProvided) { + this._results.delete(filePath); + this._promiseMap.delete(filePath); + } else { + //clear all results + this._results.clear(); + this._promiseMap.clear(); + } + }; + + /** + * Publish the diagnostics information related to current document + * @param {Object} msgObj - json object containing information associated with 'textDocument/publishDiagnostics' notification from server + */ + LintingProvider.prototype.setInspectionResults = function (msgObj) { + let diagnostics = msgObj.diagnostics, + filePath = PathConverters.uriToPath(msgObj.uri), + errors = []; + + errors = diagnostics.map(function (obj) { + return { + pos: { + line: obj.range.start.line, + ch: obj.range.start.character + }, + message: obj.message, + type: (obj.severity === 1 ? CodeInspection.Type.ERROR : (obj.severity === 2 ? CodeInspection.Type.WARNING : CodeInspection.Type.META)) + }; + }); + + this._results.set(filePath, { + errors: errors + }); + if(this._promiseMap.get(filePath)) { + this._promiseMap.get(filePath).resolve(this._results.get(filePath)); + this._promiseMap.delete(filePath); + } + if (this._validateOnType) { + var editor = EditorManager.getActiveEditor(), + docPath = editor ? editor.document.file._path : ""; + if (filePath === docPath) { + CodeInspection.requestRun(); + } + } + }; + + LintingProvider.prototype.getInspectionResultsAsync = function (fileText, filePath) { + var result = $.Deferred(); + + if (this._results.get(filePath)) { + return result.resolve(this._results.get(filePath)); + } + this._promiseMap.set(filePath, result); + return result; + }; + + LintingProvider.prototype.getInspectionResults = function (fileText, filePath) { + return this._results.get(filePath); + }; + + function serverRespToSearchModelFormat(msgObj) { + var referenceModel = {}, + result = $.Deferred(); + + if(!(msgObj && msgObj.length && msgObj.cursorPos)) { + return result.reject(); + } + referenceModel.results = {}; + referenceModel.numFiles = 0; + var fulfilled = 0; + msgObj.forEach((element, i) => { + var filePath = PathConverters.uriToPath(element.uri); + DocumentManager.getDocumentForPath(filePath) + .done(function(doc) { + var startRange = {line: element.range.start.line, ch: element.range.start.character}; + var endRange = {line: element.range.end.line, ch: element.range.end.character}; + var match = { + start: startRange, + end: endRange, + highlightOffset: 0, + line: doc.getLine(element.range.start.line) + }; + if(!referenceModel.results[filePath]) { + referenceModel.numFiles = referenceModel.numFiles + 1; + referenceModel.results[filePath] = {"matches": []}; + } + if(!referenceModel.queryInfo || msgObj.cursorPos.line === startRange.line) { + referenceModel.queryInfo = doc.getRange(startRange, endRange); + } + referenceModel.results[filePath]["matches"].push(match); + }).always(function() { + fulfilled++; + if(fulfilled === msgObj.length) { + referenceModel.numMatches = msgObj.length; + referenceModel.allResultsAvailable = true; + result.resolve(referenceModel); + } + }); + }); + return result.promise(); + } + + function ReferencesProvider(client) { + this.client = client; + } + + ReferencesProvider.prototype.setClient = setClient; + + ReferencesProvider.prototype.hasReferences = function() { + if (!this.client) { + return false; + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.referencesProvider) { + return false; + } + + return true; + }; + + ReferencesProvider.prototype.getReferences = function(hostEditor, curPos) { + var editor = hostEditor || EditorManager.getActiveEditor(), + pos = curPos || editor ? editor.getCursorPos() : null, + docPath = editor.document.file._path, + result = $.Deferred(); + + if (this.client) { + this.client.findReferences({ + filePath: docPath, + cursorPos: pos + }).done(function(msgObj){ + if(msgObj && msgObj.length) { + msgObj.cursorPos = pos; + serverRespToSearchModelFormat(msgObj) + .done(result.resolve) + .fail(result.reject); + } else { + result.reject(); + } + }).fail(function(){ + result.reject(); + }); + return result.promise(); + } + return result.reject(); + }; + + exports.CodeHintsProvider = CodeHintsProvider; + exports.ParameterHintsProvider = ParameterHintsProvider; + exports.JumpToDefProvider = JumpToDefProvider; + exports.LintingProvider = LintingProvider; + exports.ReferencesProvider = ReferencesProvider; + exports.serverRespToSearchModelFormat = serverRespToSearchModelFormat; +}); diff --git a/src/languageTools/LanguageClient/Connection.js b/src/languageTools/LanguageClient/Connection.js new file mode 100644 index 00000000000..47aa9c6747d --- /dev/null +++ b/src/languageTools/LanguageClient/Connection.js @@ -0,0 +1,134 @@ +/* + * 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 */ +/*eslint no-console: 0*/ +/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ +(function () { + "use strict"; + + var protocol = require("vscode-languageserver-protocol"); + + var Actions = { + OnClose: { + Stop: 0, + Restart: 1 + }, + OnError: { + Ignore: 0, + Stop: 1 + } + }; + + function ActionController() { + this.restartsTimes = []; + } + + ActionController.prototype.getOnErrorAction = function (errorData) { + var errorCount = errorData[2]; + + if (errorCount <= 3) { + return Actions.OnError.Ignore; + } + + return Actions.OnError.Restart; + }; + + ActionController.prototype.getOnCloseAction = function () { + var currentTime = Date.now(); + this.restartsTimes.push(currentTime); + + var numRestarts = this.restartsTimes.length; + if (numRestarts < 5) { + return Actions.OnClose.Restart; + } + + var timeBetweenFiveRestarts = this.restartsTimes[numRestarts - 1] - this.restartsTimes[0]; + if (timeBetweenFiveRestarts <= 3 * 60 * 1000) { //3 minutes + return Actions.OnClose.Stop; + } + + this.restartsTimes.shift(); + return Actions.OnClose.Restart; + }; + + function _getOnCloseHandler(connection, actionController, restartLanguageClient) { + return function () { + try { + if (connection) { + connection.dispose(); + } + } catch (error) {} + + var action = Actions.OnClose.Stop; + try { + action = actionController.getOnCloseAction(); + } catch (error) {} + + + if (action === Actions.OnClose.Restart) { + restartLanguageClient(); + } + }; + } + + function _getOnErrorHandler(actionController, stopLanguageClient) { + return function (errorData) { + var action = actionController.getOnErrorAction(errorData); + + if (action === Actions.OnError.Stop) { + stopLanguageClient(); + } + }; + } + + function Logger() {} + + Logger.prototype.error = function (message) { + console.error(message); + }; + Logger.prototype.warn = function (message) { + console.warn(message); + }; + Logger.prototype.info = function (message) { + console.info(message); + }; + Logger.prototype.log = function (message) { + console.log(message); + }; + + function createConnection(reader, writer, restartLanguageClient, stopLanguageClient) { + var logger = new Logger(), + actionController = new ActionController(), + connection = protocol.createProtocolConnection(reader, writer, logger), + errorHandler = _getOnErrorHandler(actionController, stopLanguageClient), + closeHandler = _getOnCloseHandler(connection, actionController, restartLanguageClient); + + connection.onError(errorHandler); + connection.onClose(closeHandler); + + return connection; + } + + exports.createConnection = createConnection; +}()); diff --git a/src/languageTools/LanguageClient/LanguageClient.js b/src/languageTools/LanguageClient/LanguageClient.js new file mode 100644 index 00000000000..a8e4888ed0d --- /dev/null +++ b/src/languageTools/LanguageClient/LanguageClient.js @@ -0,0 +1,232 @@ +/* + * 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, Promise, LanguageClientInfo */ +/*eslint no-console: 0*/ +/*eslint strict: ["error", "global"]*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +"use strict"; + +var ProtocolAdapter = require("./ProtocolAdapter"), + ServerUtils = require("./ServerUtils"), + Connection = require("./Connection"), + NodeToBracketsInterface = require("./NodeToBracketsInterface").NodeToBracketsInterface, + ToolingInfo = LanguageClientInfo.toolingInfo, + MESSAGE_TYPE = { + BRACKETS: "brackets", + SERVER: "server" + }; + +function validateHandler(handler) { + var retval = false; + + if (handler && typeof handler === "function") { + retval = true; + } else { + console.warn("Handler validation failed. Handler should be of type 'function'. Provided handler is of type :", typeof handler); + } + + return retval; +} + +function LanguageClient(clientName, domainManager, options) { + this._clientName = clientName; + this._bracketsInterface = null; + this._notifyBrackets = null; + this._requestBrackets = null; + this._connection = null, + this._startUpParams = null, //_projectRoot, capabilties, workspaceFolders etc. + this._initialized = false, + this._onRequestHandler = {}; + this._onNotificationHandlers = {}; + this._options = options || null; + + + this._init(domainManager); +} + + +LanguageClient.prototype._createConnection = function () { + if (!this._options || !this._options.serverOptions) { + return Promise.reject("No valid options provided for client :", this._clientName); + } + + var restartLanguageClient = this.start.bind(this), + stopLanguageClient = this.stop.bind(this); + + var serverOptions = this._options.serverOptions; + return ServerUtils.startServerAndGetConnectionArgs(serverOptions) + .then(function (connectionArgs) { + return Connection.createConnection(connectionArgs.reader, connectionArgs.writer, restartLanguageClient, stopLanguageClient); + }).catch(function (err) { + console.error("Couldn't establish connection", err); + }); +}; + +LanguageClient.prototype.setOptions = function (options) { + if (options && typeof options === "object") { + this._options = options; + } else { + console.error("Invalid options provided for client :", this._clientName); + } +}; + +LanguageClient.prototype.start = function (params) { + var self = this; + + //Check to see if a connection to a language server already exists. + if (self._connection) { + return Promise.resolve(true); + } + + self._connection = null; + self._startUpParams = params || self._startUpParams; + + //We default to standard capabilties + if (!self._startUpParams.capabilities) { + self._startUpParams.capabilities = LanguageClientInfo.defaultBracketsCapabilities; + } + + return self._createConnection() + .then(function (connection) { + connection.listen(); + self._connection = connection; + + return ProtocolAdapter.initialize(connection, self._startUpParams); + }).then(function (result) { + self._initialized = result; + ProtocolAdapter.attachOnNotificationHandlers(self._connection, self._notifyBrackets); + ProtocolAdapter.attachOnRequestHandlers(self._connection, self._requestBrackets); + ProtocolAdapter.initialized(self._connection); + return result; + }).catch(function (error) { + console.error('Starting client failed because :', error); + console.error('Couldn\'t start client :', self._clientName); + + return error; + }); +}; + +LanguageClient.prototype.stop = function () { + var self = this; + + self._initialized = false; + if (!self._connection) { + return Promise.resolve(true); + } + + + return ProtocolAdapter.shutdown(self._connection).then(function () { + ProtocolAdapter.exit(self._connection); + self._connection.dispose(); + self._connection = null; + }); +}; + +LanguageClient.prototype.request = function (params) { + var messageParams = params.params; + if (messageParams && messageParams.messageType === MESSAGE_TYPE.BRACKETS) { + if (!messageParams.type) { + console.log("Invalid brackets request"); + return Promise.reject(); + } + + var requestHandler = this._onRequestHandler[messageParams.type]; + if(validateHandler(requestHandler)) { + return requestHandler.call(null, messageParams.params); + } + console.log("No handler provided for brackets request type : ", messageParams.type); + return Promise.reject(); + } + return ProtocolAdapter.processRequest(this._connection, params); + +}; + +LanguageClient.prototype.notify = function (params) { + var messageParams = params.params; + if (messageParams && messageParams.messageType === MESSAGE_TYPE.BRACKETS) { + if (!messageParams.type) { + console.log("Invalid brackets notification"); + return; + } + + var notificationHandlers = this._onNotificationHandlers[messageParams.type]; + if(notificationHandlers && Array.isArray(notificationHandlers) && notificationHandlers.length) { + notificationHandlers.forEach(function (handler) { + if(validateHandler(handler)) { + handler.call(null, messageParams.params); + } + }); + } else { + console.log("No handlers provided for brackets notification type : ", messageParams.type); + } + } else { + ProtocolAdapter.processNotification(this._connection, params); + } +}; + +LanguageClient.prototype.addOnRequestHandler = function (type, handler) { + if (validateHandler(handler)) { + this._onRequestHandler[type] = handler; + } +}; + +LanguageClient.prototype.addOnNotificationHandler = function (type, handler) { + if (validateHandler(handler)) { + if (!this._onNotificationHandlers[type]) { + this._onNotificationHandlers[type] = []; + } + + this._onNotificationHandlers[type].push(handler); + } +}; + +LanguageClient.prototype._init = function (domainManager) { + this._bracketsInterface = new NodeToBracketsInterface(domainManager, this._clientName); + + //Expose own methods for interfaceing. All these are async except notify. + this._bracketsInterface.registerMethods([ + { + methodName: ToolingInfo.LANGUAGE_SERVICE.START, + methodHandle: this.start.bind(this) + }, + { + methodName: ToolingInfo.LANGUAGE_SERVICE.STOP, + methodHandle: this.stop.bind(this) + }, + { + methodName: ToolingInfo.LANGUAGE_SERVICE.REQUEST, + methodHandle: this.request.bind(this) + }, + { + methodName: ToolingInfo.LANGUAGE_SERVICE.NOTIFY, + methodHandle: this.notify.bind(this) + } + ]); + + //create function interfaces for Brackets + this._notifyBrackets = this._bracketsInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.NOTIFY); + this._requestBrackets = this._bracketsInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.REQUEST, true); +}; + +exports.LanguageClient = LanguageClient; diff --git a/src/languageTools/LanguageClient/NodeToBracketsInterface.js b/src/languageTools/LanguageClient/NodeToBracketsInterface.js new file mode 100644 index 00000000000..80d85e96c12 --- /dev/null +++ b/src/languageTools/LanguageClient/NodeToBracketsInterface.js @@ -0,0 +1,213 @@ +/* + * 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 require, Promise, exports*/ +/*eslint no-invalid-this: 0*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +(function () { + + "use strict"; + + var EventEmitter = require("events"), + bracketsEventHandler = new EventEmitter(); + + /** https://gist.github.com/LeverOne/1308368 */ + /*eslint-disable */ + function _generateUUID() { + var result, + numericSeed; + for ( + result = numericSeed = ''; + numericSeed++ < 36; + result += numericSeed * 51 & 52 ? (numericSeed ^ 15 ? 8 ^ Math.random() * (numericSeed ^ 20 ? 16 : 4) : 4).toString(16) : '-' + ); + + return result; + } + /*eslint-enable */ + + function NodeToBracketsInterface(domainManager, domainName) { + this.domainManager = domainManager; + this.domainName = domainName; + this.nodeFn = {}; + + this._registerDataEvents(domainManager, domainName); + } + + NodeToBracketsInterface.prototype.processRequest = function (params) { + var methodName = params.method; + if (this.nodeFn[methodName]) { + var method = this.nodeFn[methodName]; + return method.call(null, params.params); + } + }; + + NodeToBracketsInterface.prototype.processAsyncRequest = function (params, resolver) { + var methodName = params.method; + if (this.nodeFn[methodName]) { + var method = this.nodeFn[methodName]; + method.call(null, params.params) //The Async function should return a promise + .then(function (result) { + resolver(null, result); + }).catch(function (err) { + resolver(err, null); + }); + } + }; + + NodeToBracketsInterface.prototype.processResponse = function (params) { + if (params.requestId) { + if (params.error) { + bracketsEventHandler.emit(params.requestId, params.error); + } else { + bracketsEventHandler.emit(params.requestId, false, params.params); + } + } else { + bracketsEventHandler.emit(params.requestId, "error"); + } + }; + + NodeToBracketsInterface.prototype.createInterface = function (methodName, respond) { + var self = this; + return function (params) { + var callObject = { + method: methodName, + params: params + }; + + var retval = undefined; + if (respond) { + var requestId = _generateUUID(); + + callObject["respond"] = true; + callObject["requestId"] = requestId; + + self.domainManager.emitEvent(self.domainName, "data", callObject); + + retval = new Promise(function (resolve, reject) { + bracketsEventHandler.once(requestId, function (err, response) { + if (err) { + reject(err); + } else { + resolve(response); + } + }); + }); + } else { + self.domainManager.emitEvent(self.domainName, "data", callObject); + } + return retval; + }; + }; + + NodeToBracketsInterface.prototype.registerMethod = function (methodName, methodHandle) { + var self = this; + if (methodName && methodHandle && + typeof methodName === "string" && typeof methodHandle === "function") { + self.nodeFn[methodName] = methodHandle; + } + }; + + NodeToBracketsInterface.prototype.registerMethods = function (methodList) { + var self = this; + methodList.forEach(function (methodObj) { + self.registerMethod(methodObj.methodName, methodObj.methodHandle); + }); + }; + + NodeToBracketsInterface.prototype._registerDataEvents = function (domainManager, domainName) { + if (!domainManager.hasDomain(domainName)) { + domainManager.registerDomain(domainName, { + major: 0, + minor: 1 + }); + } + + domainManager.registerCommand( + domainName, + "data", + this.processRequest.bind(this), + false, + "Receives sync request from brackets", + [ + { + name: "params", + type: "object", + description: "json object containing message info" + } + ], + [] + ); + + domainManager.registerCommand( + domainName, + "response", + this.processResponse.bind(this), + false, + "Receives response from brackets for an earlier request", + [ + { + name: "params", + type: "object", + description: "json object containing message info" + } + ], + [] + ); + + domainManager.registerCommand( + domainName, + "asyncData", + this.processAsyncRequest.bind(this), + true, + "Receives async call request from brackets", + [ + { + name: "params", + type: "object", + description: "json object containing message info" + }, + { + name: "resolver", + type: "function", + description: "callback required to resolve the async request" + } + ], + [] + ); + + domainManager.registerEvent( + domainName, + "data", + [ + { + name: "params", + type: "object", + description: "json object containing message info to pass to brackets" + } + ] + ); + }; + + exports.NodeToBracketsInterface = NodeToBracketsInterface; +}()); diff --git a/src/languageTools/LanguageClient/ProtocolAdapter.js b/src/languageTools/LanguageClient/ProtocolAdapter.js new file mode 100644 index 00000000000..e9eabb35aaa --- /dev/null +++ b/src/languageTools/LanguageClient/ProtocolAdapter.js @@ -0,0 +1,398 @@ +/* + * 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 LanguageClientInfo*/ +/*eslint-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint no-fallthrough: 0*/ +"use strict"; + +var protocol = require("vscode-languageserver-protocol"), + Utils = require("./Utils"), + ToolingInfo = LanguageClientInfo.toolingInfo, + MESSAGE_FORMAT = { + BRACKETS: "brackets", + LSP: "lsp" + }; + +function _constructParamsAndRelay(relay, type, params) { + var _params = null, + handler = null; + + //Check for param object format. We won't change anything if the object is preformatted. + if (params.format === MESSAGE_FORMAT.LSP) { + params.format = undefined; + _params = JSON.parse(JSON.stringify(params)); + } + + switch (type) { + case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST: + return sendCustomRequest(relay, params.type, params.params); + case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION: + { + sendCustomNotification(relay, params.type, params.params); + break; + } + case ToolingInfo.SERVICE_REQUESTS.SHOW_SELECT_MESSAGE: + case ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST: + case ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST: + case ToolingInfo.SERVICE_REQUESTS.PROJECT_FOLDERS_REQUEST: + { + _params = { + type: type, + params: params + }; + return relay(_params); + } + case ToolingInfo.SERVICE_NOTIFICATIONS.SHOW_MESSAGE: + case ToolingInfo.SERVICE_NOTIFICATIONS.LOG_MESSAGE: + case ToolingInfo.SERVICE_NOTIFICATIONS.TELEMETRY: + case ToolingInfo.SERVICE_NOTIFICATIONS.DIAGNOSTICS: + { + _params = { + type: type, + params: params + }; + relay(_params); + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED: + { + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath), + languageId: params.languageId, + version: 1, + text: params.fileContent + } + }; + didOpenTextDocument(relay, _params); + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED: + { + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath), + version: 1 + }, + contentChanges: [{ + text: params.fileContent + }] + }; + didChangeTextDocument(relay, _params); + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED: + { + if (!_params) { + _params = { + textDocument: { + uri: Utils.pathToUri(params.filePath) + } + }; + + if (params.fileContent) { + _params['text'] = params.fileContent; + } + } + didSaveTextDocument(relay, _params); + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED: + { + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath) + } + }; + + didCloseTextDocument(relay, _params); + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED: + { + var foldersAdded = params.foldersAdded || [], + foldersRemoved = params.foldersRemoved || []; + + foldersAdded = Utils.convertToWorkspaceFolders(foldersAdded); + foldersRemoved = Utils.convertToWorkspaceFolders(foldersRemoved); + + _params = _params || { + event: { + added: foldersAdded, + removed: foldersRemoved + } + }; + didChangeWorkspaceFolders(relay, _params); + break; + } + case ToolingInfo.FEATURES.CODE_HINTS: + handler = completion; + case ToolingInfo.FEATURES.PARAMETER_HINTS: + handler = handler || signatureHelp; + case ToolingInfo.FEATURES.JUMP_TO_DECLARATION: + handler = handler || gotoDeclaration; + case ToolingInfo.FEATURES.JUMP_TO_DEFINITION: + handler = handler || gotoDefinition; + case ToolingInfo.FEATURES.JUMP_TO_IMPL: + { + handler = handler || gotoImplementation; + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath) + }, + position: Utils.convertToLSPPosition(params.cursorPos) + }; + + return handler(relay, _params); + } + case ToolingInfo.FEATURES.CODE_HINT_INFO: + { + return completionItemResolve(relay, params); + } + case ToolingInfo.FEATURES.FIND_REFERENCES: + { + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath) + }, + position: Utils.convertToLSPPosition(params.cursorPos), + context: { + includeDeclaration: params.includeDeclaration + } + }; + + return findReferences(relay, _params); + } + case ToolingInfo.FEATURES.DOCUMENT_SYMBOLS: + { + _params = _params || { + textDocument: { + uri: Utils.pathToUri(params.filePath) + } + }; + + return documentSymbol(relay, _params); + } + case ToolingInfo.FEATURES.PROJECT_SYMBOLS: + { + _params = _params || { + query: params.query + }; + + return workspaceSymbol(relay, _params); + } + } +} + +/** For custom messages */ +function onCustom(connection, type, handler) { + connection.onNotification(type, handler); +} + +function sendCustomRequest(connection, type, params) { + return connection.sendRequest(type, params); +} + +function sendCustomNotification(connection, type, params) { + connection.sendNotification(type, params); +} + +/** For Notification messages */ +function didOpenTextDocument(connection, params) { + connection.sendNotification(protocol.DidOpenTextDocumentNotification.type, params); +} + +function didChangeTextDocument(connection, params) { + connection.sendNotification(protocol.DidChangeTextDocumentNotification.type, params); +} + +function didCloseTextDocument(connection, params) { + connection.sendNotification(protocol.DidCloseTextDocumentNotification.type, params); +} + +function didSaveTextDocument(connection, params) { + connection.sendNotification(protocol.DidSaveTextDocumentNotification.type, params); +} + +function didChangeWorkspaceFolders(connection, params) { + connection.sendNotification(protocol.DidChangeWorkspaceFoldersNotification.type, params); +} + +/** For Request messages */ +function completion(connection, params) { + return connection.sendRequest(protocol.CompletionRequest.type, params); +} + +function completionItemResolve(connection, params) { + return connection.sendRequest(protocol.CompletionResolveRequest.type, params); +} + +function signatureHelp(connection, params) { + return connection.sendRequest(protocol.SignatureHelpRequest.type, params); +} + +function gotoDefinition(connection, params) { + return connection.sendRequest(protocol.DefinitionRequest.type, params); +} + +function gotoDeclaration(connection, params) { + return connection.sendRequest(protocol.DeclarationRequest.type, params); +} + +function gotoImplementation(connection, params) { + return connection.sendRequest(protocol.ImplementationRequest.type, params); +} + +function findReferences(connection, params) { + return connection.sendRequest(protocol.ReferencesRequest.type, params); +} + +function documentSymbol(connection, params) { + return connection.sendRequest(protocol.DocumentSymbolRequest.type, params); +} + +function workspaceSymbol(connection, params) { + return connection.sendRequest(protocol.WorkspaceSymbolRequest.type, params); +} + +/** + * Server commands + */ +function initialize(connection, params) { + var rootPath = params.rootPath, + workspaceFolders = params.rootPaths; + + if(!rootPath && workspaceFolders && Array.isArray(workspaceFolders)) { + rootPath = workspaceFolders[0]; + } + + if (!workspaceFolders) { + workspaceFolders = [rootPath]; + } + + if (workspaceFolders.length) { + workspaceFolders = Utils.convertToWorkspaceFolders(workspaceFolders); + } + + var _params = { + rootPath: rootPath, + rootUri: Utils.pathToUri(rootPath), + processId: process.pid, + capabilities: params.capabilities, + workspaceFolders: workspaceFolders + }; + + return connection.sendRequest(protocol.InitializeRequest.type, _params); +} + +function initialized(connection) { + connection.sendNotification(protocol.InitializedNotification.type); +} + +function shutdown(connection) { + return connection.sendRequest(protocol.ShutdownRequest.type); +} + +function exit(connection) { + connection.sendNotification(protocol.ExitNotification.type); +} + +function processRequest(connection, message) { + return _constructParamsAndRelay(connection, message.type, message.params); +} + +function processNotification(connection, message) { + _constructParamsAndRelay(connection, message.type, message.params); +} + +function attachOnNotificationHandlers(connection, handler) { + function _callbackFactory(type) { + switch (type) { + case protocol.ShowMessageNotification.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_NOTIFICATIONS.SHOW_MESSAGE); + case protocol.LogMessageNotification.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_NOTIFICATIONS.LOG_MESSAGE); + case protocol.TelemetryEventNotification.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_NOTIFICATIONS.TELEMETRY); + case protocol.PublishDiagnosticsNotification.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_NOTIFICATIONS.DIAGNOSTICS); + } + } + + connection.onNotification(protocol.ShowMessageNotification.type, _callbackFactory(protocol.ShowMessageNotification.type)); + connection.onNotification(protocol.LogMessageNotification.type, _callbackFactory(protocol.LogMessageNotification.type)); + connection.onNotification(protocol.TelemetryEventNotification.type, _callbackFactory(protocol.TelemetryEventNotification.type)); + connection.onNotification(protocol.PublishDiagnosticsNotification.type, _callbackFactory(protocol.PublishDiagnosticsNotification.type)); + connection.onNotification(function (type, params) { + var _params = { + type: type, + params: params + }; + handler(_params); + }); +} + +function attachOnRequestHandlers(connection, handler) { + function _callbackFactory(type) { + switch (type) { + case protocol.ShowMessageRequest.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_REQUESTS.SHOW_SELECT_MESSAGE); + case protocol.RegistrationRequest.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST); + case protocol.UnregistrationRequest.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST); + case protocol.WorkspaceFoldersRequest.type: + return _constructParamsAndRelay.bind(null, handler, ToolingInfo.SERVICE_REQUESTS.PROJECT_FOLDERS_REQUEST); + } + } + + connection.onRequest(protocol.ShowMessageRequest.type, _callbackFactory(protocol.ShowMessageRequest.type)); + connection.onRequest(protocol.RegistrationRequest.type, _callbackFactory(protocol.RegistrationRequest.type)); + // See https://github.com/Microsoft/vscode-languageserver-node/issues/199 + connection.onRequest("client/registerFeature", _callbackFactory(protocol.RegistrationRequest.type)); + connection.onRequest(protocol.UnregistrationRequest.type, _callbackFactory(protocol.UnregistrationRequest.type)); + // See https://github.com/Microsoft/vscode-languageserver-node/issues/199 + connection.onRequest("client/unregisterFeature", _callbackFactory(protocol.UnregistrationRequest.type)); + connection.onRequest(protocol.WorkspaceFoldersRequest.type, _callbackFactory(protocol.WorkspaceFoldersRequest.type)); + connection.onRequest(function (type, params) { + var _params = { + type: type, + params: params + }; + return handler(_params); + }); +} + +exports.initialize = initialize; +exports.initialized = initialized; +exports.shutdown = shutdown; +exports.exit = exit; +exports.onCustom = onCustom; +exports.sendCustomRequest = sendCustomRequest; +exports.sendCustomNotification = sendCustomNotification; +exports.processRequest = processRequest; +exports.processNotification = processNotification; +exports.attachOnNotificationHandlers = attachOnNotificationHandlers; +exports.attachOnRequestHandlers = attachOnRequestHandlers; diff --git a/src/languageTools/LanguageClient/ServerUtils.js b/src/languageTools/LanguageClient/ServerUtils.js new file mode 100644 index 00000000000..3e865a5c1a7 --- /dev/null +++ b/src/languageTools/LanguageClient/ServerUtils.js @@ -0,0 +1,427 @@ +/* + * 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, process, Promise, __dirname, global*/ +/*eslint no-console: 0*/ +/*eslint no-fallthrough: 0*/ +/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ +(function () { + "use strict"; + + var protocol = require("vscode-languageserver-protocol"), + cp = require("child_process"), + fs = require("fs"); + + var CommunicationTypes = { + NodeIPC: { + type: "ipc", + flag: "--node-ipc" + }, + StandardIO: { + type: "stdio", + flag: "--stdio" + }, + Pipe: { + type: "pipe", + flag: "--pipe" + }, + Socket: { + type: "socket", + flag: "--socket" + } + }, + CLIENT_PROCESS_ID_FLAG = "--clientProcessId"; + + function addCommunicationArgs(communication, processArgs, isRuntime) { + switch (communication) { + case CommunicationTypes.NodeIPC.type: + { + if (isRuntime) { + processArgs.options.stdio = [null, null, null, 'ipc']; + processArgs.args.push(CommunicationTypes.NodeIPC.flag); + } else { + processArgs.args.push(CommunicationTypes.NodeIPC.flag); + } + break; + } + case CommunicationTypes.StandardIO.type: + { + processArgs.args.push(CommunicationTypes.StandardIO.flag); + break; + } + case CommunicationTypes.Pipe.type: + { + var pipeName = protocol.generateRandomPipeName(), + pipeflag = CommunicationTypes.Pipe.flag + "=" + pipeName.toString(); + + processArgs.args.push(pipeflag); + processArgs.pipeName = pipeName; + break; + } + default: + { + if (communication && communication.type === CommunicationTypes.Socket.type) { + var socketFlag = CommunicationTypes.Socket.flag + "=" + communication.port.toString(); + processArgs.args.push(socketFlag); + } + } + } + + var clientProcessIdFlag = CLIENT_PROCESS_ID_FLAG + "=" + process.pid.toString(); + processArgs.args.push(clientProcessIdFlag); + } + + function _getEnvironment(env) { + if (!env) { + return process.env; + } + + //Combine env vars + var result = Object.assign({}, process.env, env); + return result; + } + + function _createReaderAndWriteByCommunicationType(resp, type) { + var retval = null; + + switch (type) { + case CommunicationTypes.NodeIPC.type: + { + if (resp.process) { + resp.process.stderr.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.error('[Server logs @ stderr] "%s"', String(data)); + } + }); + + resp.process.stdout.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.info('[Server logs @ stdout] "%s"', String(data)); + } + }); + + retval = { + reader: new protocol.IPCMessageReader(resp.process), + writer: new protocol.IPCMessageWriter(resp.process) + }; + } + break; + } + case CommunicationTypes.StandardIO.type: + { + if (resp.process) { + resp.process.stderr.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.error('[Server logs @ stderr] "%s"', String(data)); + } + }); + + retval = { + reader: new protocol.StreamMessageReader(resp.process.stdout), + writer: new protocol.StreamMessageWriter(resp.process.stdin) + }; + } + break; + } + case CommunicationTypes.Pipe.type: + case CommunicationTypes.Socket.type: + { + if (resp.reader && resp.writer && resp.process) { + resp.process.stderr.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.error('[Server logs @ stderr] "%s"', String(data)); + } + }); + + resp.process.stdout.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.info('[Server logs @ stdout] "%s"', String(data)); + } + }); + + retval = { + reader: resp.reader, + writer: resp.writer + }; + } + } + } + + return retval; + } + + function _createReaderAndWriter(resp) { + var retval = null; + + if (!resp) { + return retval; + } + + if (resp.reader && resp.writer) { + retval = { + reader: resp.reader, + writer: resp.writer + }; + } else if (resp.process) { + retval = { + reader: new protocol.StreamMessageReader(resp.process.stdout), + writer: new protocol.StreamMessageWriter(resp.process.stdin) + }; + + resp.process.stderr.on('data', function (data) { + if (global.LanguageClientInfo.preferences.showServerLogsInConsole) { + console.error('[Server logs @ stderr] "%s"', String(data)); + } + }); + } + + return retval; + } + + function _isServerProcessValid(serverProcess) { + if (!serverProcess || !serverProcess.pid) { + return false; + } + + return true; + } + + function _startServerAndGetTransports(communication, processArgs, isRuntime) { + return new Promise(function (resolve, reject) { + var serverProcess = null, + result = null, + protocolTransport = null, + type = typeof communication === "object" ? communication.type : communication; + + var processFunc = isRuntime ? cp.spawn : cp.fork; + + switch (type) { + case CommunicationTypes.NodeIPC.type: + case CommunicationTypes.StandardIO.type: + { + serverProcess = processFunc(processArgs.primaryArg, processArgs.args, processArgs.options); + if (_isServerProcessValid(serverProcess)) { + result = _createReaderAndWriteByCommunicationType({ + process: serverProcess + }, type); + + resolve(result); + } else { + reject(null); + } + break; + } + case CommunicationTypes.Pipe.type: + { + protocolTransport = protocol.createClientPipeTransport(processArgs.pipeName); + } + case CommunicationTypes.Socket.type: + { + if (communication && communication.type === CommunicationTypes.Socket.type) { + protocolTransport = protocol.createClientSocketTransport(communication.port); + } + + if (!protocolTransport) { + reject("Invalid Communications Object. Can't create connection with server"); + return; + } + + protocolTransport.then(function (transportObj) { + serverProcess = processFunc(processArgs.primaryArg, processArgs.args, processArgs.options); + if (_isServerProcessValid(serverProcess)) { + transportObj.onConnected().then(function (protocolObj) { + result = _createReaderAndWriteByCommunicationType({ + process: serverProcess, + reader: protocolObj[0], + writer: protocolObj[1] + }, type); + + resolve(result); + }).catch(reject); + } + }).catch(reject); + } + } + }); + } + + function _handleOtherRuntime(serverOptions) { + function _getArguments(sOptions) { + var args = []; + + if (sOptions.options && sOptions.options.execArgv) { + args = args.concat(sOptions.options.execArgv); + } + + args.push(sOptions.module); + if (sOptions.args) { + args = args.concat(sOptions.args); + } + + return args; + } + + function _getOptions(sOptions) { + var cwd = undefined, + env = undefined; + + if (sOptions.options) { + if (sOptions.options.cwd) { + try { + if (fs.lstatSync(sOptions.options.cwd).isDirectory(sOptions.options.cwd)) { + cwd = sOptions.options.cwd; + } + } catch (e) {} + } + + cwd = cwd || __dirname; + if (sOptions.options.env) { + env = sOptions.options.env; + } + } + + var options = { + cwd: cwd, + env: _getEnvironment(env) + }; + + return options; + } + + var communication = serverOptions.communication || CommunicationTypes.StandardIO.type, + args = _getArguments(serverOptions), + options = _getOptions(serverOptions), + processArgs = { + args: args, + options: options, + primaryArg: serverOptions.runtime + }; + + addCommunicationArgs(communication, processArgs, true); + return _startServerAndGetTransports(communication, processArgs, true); + } + + function _handleNodeRuntime(serverOptions) { + function _getArguments(sOptions) { + var args = []; + + if (sOptions.args) { + args = args.concat(sOptions.args); + } + + return args; + } + + function _getOptions(sOptions) { + var cwd = undefined; + + if (sOptions.options) { + if (sOptions.options.cwd) { + try { + if (fs.lstatSync(sOptions.options.cwd).isDirectory(sOptions.options.cwd)) { + cwd = sOptions.options.cwd; + } + } catch (e) {} + } + cwd = cwd || __dirname; + } + + var options = Object.assign({}, sOptions.options); + options.cwd = cwd, + options.execArgv = options.execArgv || []; + options.silent = true; + + return options; + } + + var communication = serverOptions.communication || CommunicationTypes.StandardIO.type, + args = _getArguments(serverOptions), + options = _getOptions(serverOptions), + processArgs = { + args: args, + options: options, + primaryArg: serverOptions.module + }; + + addCommunicationArgs(communication, processArgs, false); + return _startServerAndGetTransports(communication, processArgs, false); + } + + + function _handleServerFunction(func) { + return func().then(function (resp) { + var result = _createReaderAndWriter(resp); + + return result; + }); + } + + function _handleModules(serverOptions) { + if (serverOptions.runtime) { + return _handleOtherRuntime(serverOptions); + } + return _handleNodeRuntime(serverOptions); + + } + + function _handleExecutable(serverOptions) { + return new Promise(function (resolve, reject) { + var command = serverOptions.command, + args = serverOptions.args, + options = Object.assign({}, serverOptions.options); + + var serverProcess = cp.spawn(command, args, options); + if (!serverProcess || !serverProcess.pid) { + reject("Failed to launch server using command :", command); + } + + var result = _createReaderAndWriter({ + process: serverProcess, + detached: !!options.detached + }); + + if (result) { + resolve(result); + } else { + reject(result); + } + }); + } + + function startServerAndGetConnectionArgs(serverOptions) { + if (typeof serverOptions === "function") { + return _handleServerFunction(serverOptions); + } else if (typeof serverOptions === "object") { + if (serverOptions.module) { + return _handleModules(serverOptions); + } else if (serverOptions.command) { + return _handleExecutable(serverOptions); + } + } + + return Promise.reject(null); + } + + + exports.startServerAndGetConnectionArgs = startServerAndGetConnectionArgs; +}()); diff --git a/src/languageTools/LanguageClient/Utils.js b/src/languageTools/LanguageClient/Utils.js new file mode 100644 index 00000000000..28df1c0b682 --- /dev/null +++ b/src/languageTools/LanguageClient/Utils.js @@ -0,0 +1,88 @@ +/* + * 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-env es6, node*/ +"use strict"; + +var nodeURL = require("url"), + path = require("path"); + +function pathToUri(filePath) { + var newPath = convertWinToPosixPath(filePath); + if (newPath[0] !== '/') { + newPath = `/${newPath}`; + } + return encodeURI(`file://${newPath}`).replace(/[?#]/g, encodeURIComponent); +} + +function uriToPath(uri) { + var url = nodeURL.URL.parse(uri); + if (url.protocol !== 'file:' || url.path === undefined) { + return uri; + } + + let filePath = decodeURIComponent(url.path); + if (process.platform === 'win32') { + if (filePath[0] === '/') { + filePath = filePath.substr(1); + } + return filePath; + } + return filePath; +} + +function convertPosixToWinPath(filePath) { + return filePath.replace(/\//g, '\\'); +} + +function convertWinToPosixPath(filePath) { + return filePath.replace(/\\/g, '/'); +} + +function convertToLSPPosition(pos) { + return { + line: pos.line, + character: pos.ch + }; +} + +function convertToWorkspaceFolders(paths) { + var workspaceFolders = paths.map(function (folderPath) { + var uri = pathToUri(folderPath), + name = path.basename(folderPath); + + return { + uri: uri, + name: name + }; + }); + + return workspaceFolders; +} + +exports.uriToPath = uriToPath; +exports.pathToUri = pathToUri; +exports.convertPosixToWinPath = convertPosixToWinPath; +exports.convertWinToPosixPath = convertWinToPosixPath; +exports.convertToLSPPosition = convertToLSPPosition; +exports.convertToWorkspaceFolders = convertToWorkspaceFolders; diff --git a/src/languageTools/LanguageClient/package.json b/src/languageTools/LanguageClient/package.json new file mode 100644 index 00000000000..2fdc6b6094a --- /dev/null +++ b/src/languageTools/LanguageClient/package.json @@ -0,0 +1,19 @@ +{ + "name": "brackets-language-client", + "version": "1.0.0", + "description": "Brackets language client interface for Language Server Protocol", + "main": "LanguageClient.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "LSP", + "LanguageClient", + "Brackets" + ], + "author": "Adobe", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "^3.14.1" + } +} diff --git a/src/languageTools/LanguageClientWrapper.js b/src/languageTools/LanguageClientWrapper.js new file mode 100644 index 00000000000..edd9a6b9602 --- /dev/null +++ b/src/languageTools/LanguageClientWrapper.js @@ -0,0 +1,668 @@ +/* + * 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 no-console: 0*/ +/*eslint indent: 0*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + "use strict"; + + var ToolingInfo = JSON.parse(require("text!languageTools/ToolingInfo.json")), + MESSAGE_FORMAT = { + BRACKETS: "brackets", + LSP: "lsp" + }; + + function _addTypeInformation(type, params) { + return { + type: type, + params: params + }; + } + + function hasValidProp(obj, prop) { + return (obj && obj[prop] !== undefined && obj[prop] !== null); + } + + function hasValidProps(obj, props) { + var retval = !!obj, + len = props.length, + i; + + for (i = 0; retval && (i < len); i++) { + retval = (retval && obj[props[i]] !== undefined && obj[props[i]] !== null); + } + + return retval; + } + /* + RequestParams creator - sendNotifications/request + */ + function validateRequestParams(type, params) { + var validatedParams = null; + + params = params || {}; + + //Don't validate if the formatting is done by the caller + if (params.format === MESSAGE_FORMAT.LSP) { + return params; + } + + switch (type) { + case ToolingInfo.LANGUAGE_SERVICE.START: + { + if (hasValidProp(params, "rootPaths") || hasValidProp(params, "rootPath")) { + validatedParams = params; + validatedParams.capabilities = validatedParams.capabilities || false; + } + break; + } + case ToolingInfo.FEATURES.CODE_HINTS: + case ToolingInfo.FEATURES.PARAMETER_HINTS: + case ToolingInfo.FEATURES.JUMP_TO_DECLARATION: + case ToolingInfo.FEATURES.JUMP_TO_DEFINITION: + case ToolingInfo.FEATURES.JUMP_TO_IMPL: + { + if (hasValidProps(params, ["filePath", "cursorPos"])) { + validatedParams = params; + } + break; + } + case ToolingInfo.FEATURES.CODE_HINT_INFO: + { + validatedParams = params; + break; + } + case ToolingInfo.FEATURES.FIND_REFERENCES: + { + if (hasValidProps(params, ["filePath", "cursorPos"])) { + validatedParams = params; + validatedParams.includeDeclaration = validatedParams.includeDeclaration || false; + } + break; + } + case ToolingInfo.FEATURES.DOCUMENT_SYMBOLS: + { + if (hasValidProp(params, "filePath")) { + validatedParams = params; + } + break; + } + case ToolingInfo.FEATURES.PROJECT_SYMBOLS: + { + if (hasValidProp(params, "query") && typeof params.query === "string") { + validatedParams = params; + } + break; + } + case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST: + { + validatedParams = params; + } + } + + return validatedParams; + } + + /* + ReponseParams transformer - used by OnNotifications + */ + function validateNotificationParams(type, params) { + var validatedParams = null; + + params = params || {}; + + //Don't validate if the formatting is done by the caller + if (params.format === MESSAGE_FORMAT.LSP) { + return params; + } + + switch (type) { + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED: + { + if (hasValidProps(params, ["filePath", "fileContent", "languageId"])) { + validatedParams = params; + } + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED: + { + if (hasValidProps(params, ["filePath", "fileContent"])) { + validatedParams = params; + } + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED: + { + if (hasValidProp(params, "filePath")) { + validatedParams = params; + } + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED: + { + if (hasValidProp(params, "filePath")) { + validatedParams = params; + } + break; + } + case ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED: + { + if (hasValidProps(params, ["foldersAdded", "foldersRemoved"])) { + validatedParams = params; + } + break; + } + case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION: + { + validatedParams = params; + } + } + + return validatedParams; + } + + function validateHandler(handler) { + var retval = false; + + if (handler && typeof handler === "function") { + retval = true; + } else { + console.warn("Handler validation failed. Handler should be of type 'function'. Provided handler is of type :", typeof handler); + } + + return retval; + } + + function LanguageClientWrapper(name, path, domainInterface, languages) { + this._name = name; + this._path = path; + this._domainInterface = domainInterface; + this._languages = languages || []; + this._startClient = null; + this._stopClient = null; + this._notifyClient = null; + this._requestClient = null; + this._onRequestHandler = {}; + this._onNotificationHandlers = {}; + this._dynamicCapabilities = {}; + this._serverCapabilities = {}; + + //Initialize with keys for brackets events we want to tap into. + this._onEventHandlers = { + "activeEditorChange": [], + "projectOpen": [], + "beforeProjectClose": [], + "dirtyFlagChange": [], + "documentChange": [], + "fileNameChange": [], + "beforeAppClose": [] + }; + + this._init(); + } + + LanguageClientWrapper.prototype._init = function () { + this._domainInterface.registerMethods([ + { + methodName: ToolingInfo.LANGUAGE_SERVICE.REQUEST, + methodHandle: this._onRequestDelegator.bind(this) + }, + { + methodName: ToolingInfo.LANGUAGE_SERVICE.NOTIFY, + methodHandle: this._onNotificationDelegator.bind(this) + } + ]); + + //create function interfaces + this._startClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.START, true); + this._stopClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.STOP, true); + this._notifyClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.NOTIFY); + this._requestClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.REQUEST, true); + }; + + LanguageClientWrapper.prototype._onRequestDelegator = function (params) { + if (!params || !params.type) { + console.log("Invalid server request"); + return $.Deferred().reject(); + } + + var requestHandler = this._onRequestHandler[params.type]; + if (params.type === ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST) { + return this._registrationShim(params.params, requestHandler); + } + + if (params.type === ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST) { + return this._unregistrationShim(params.params, requestHandler); + } + + if (validateHandler(requestHandler)) { + return requestHandler.call(null, params.params); + } + console.log("No handler provided for server request type : ", params.type); + return $.Deferred().reject(); + + }; + + LanguageClientWrapper.prototype._onNotificationDelegator = function (params) { + if (!params || !params.type) { + console.log("Invalid server notification"); + return; + } + + var notificationHandlers = this._onNotificationHandlers[params.type]; + if (notificationHandlers && Array.isArray(notificationHandlers) && notificationHandlers.length) { + notificationHandlers.forEach(function (handler) { + if (validateHandler(handler)) { + handler.call(null, params.params); + } + }); + } else { + console.log("No handlers provided for server notification type : ", params.type); + } + }; + + LanguageClientWrapper.prototype._request = function (type, params) { + params = validateRequestParams(type, params); + if (params) { + params = _addTypeInformation(type, params); + return this._requestClient(params); + } + + console.log("Invalid Parameters provided for request type : ", type); + return $.Deferred().reject(); + }; + + LanguageClientWrapper.prototype._notify = function (type, params) { + params = validateNotificationParams(type, params); + if (params) { + params = _addTypeInformation(type, params); + this._notifyClient(params); + } else { + console.log("Invalid Parameters provided for notification type : ", type); + } + }; + + LanguageClientWrapper.prototype._addOnRequestHandler = function (type, handler) { + if (validateHandler(handler)) { + this._onRequestHandler[type] = handler; + } + }; + + LanguageClientWrapper.prototype._addOnNotificationHandler = function (type, handler) { + if (validateHandler(handler)) { + if (!this._onNotificationHandlers[type]) { + this._onNotificationHandlers[type] = []; + } + + this._onNotificationHandlers[type].push(handler); + } + }; + + /** + Requests + */ + //start + LanguageClientWrapper.prototype.start = function (params) { + params = validateRequestParams(ToolingInfo.LANGUAGE_SERVICE.START, params); + if (params) { + var self = this; + return this._startClient(params) + .then(function (result) { + self.setServerCapabilities(result.capabilities); + return $.Deferred().resolve(result); + }, function (err) { + return $.Deferred().reject(err); + }); + } + + console.log("Invalid Parameters provided for request type : start"); + return $.Deferred().reject(); + }; + + //shutdown + LanguageClientWrapper.prototype.stop = function () { + return this._stopClient(); + }; + + //restart + LanguageClientWrapper.prototype.restart = function (params) { + var self = this; + return this.stop().then(function () { + return self.start(params); + }); + }; + + /** + textDocument requests + */ + //completion + LanguageClientWrapper.prototype.requestHints = function (params) { + return this._request(ToolingInfo.FEATURES.CODE_HINTS, params) + .then(function(response) { + if(response && response.items && response.items.length) { + logAnalyticsData("CODE_HINTS"); + } + return $.Deferred().resolve(response); + }, function(err) { + return $.Deferred().reject(err); + }); + }; + + //completionItemResolve + LanguageClientWrapper.prototype.getAdditionalInfoForHint = function (params) { + return this._request(ToolingInfo.FEATURES.CODE_HINT_INFO, params); + }; + + //signatureHelp + LanguageClientWrapper.prototype.requestParameterHints = function (params) { + return this._request(ToolingInfo.FEATURES.PARAMETER_HINTS, params) + .then(function(response) { + if (response && response.signatures && response.signatures.length) { + logAnalyticsData("PARAM_HINTS"); + } + return $.Deferred().resolve(response); + }, function(err) { + return $.Deferred().reject(err); + }); + }; + + //gotoDefinition + LanguageClientWrapper.prototype.gotoDefinition = function (params) { + return this._request(ToolingInfo.FEATURES.JUMP_TO_DEFINITION, params) + .then(function(response) { + if(response && response.range) { + logAnalyticsData("JUMP_TO_DEF"); + } + return $.Deferred().resolve(response); + }, function(err) { + return $.Deferred().reject(err); + }); + }; + + //gotoDeclaration + LanguageClientWrapper.prototype.gotoDeclaration = function (params) { + return this._request(ToolingInfo.FEATURES.JUMP_TO_DECLARATION, params); + }; + + //gotoImplementation + LanguageClientWrapper.prototype.gotoImplementation = function (params) { + return this._request(ToolingInfo.FEATURES.JUMP_TO_IMPL, params); + }; + + //findReferences + LanguageClientWrapper.prototype.findReferences = function (params) { + return this._request(ToolingInfo.FEATURES.FIND_REFERENCES, params); + }; + + //documentSymbol + LanguageClientWrapper.prototype.requestSymbolsForDocument = function (params) { + return this._request(ToolingInfo.FEATURES.DOCUMENT_SYMBOLS, params); + }; + + /** + workspace requests + */ + //workspaceSymbol + LanguageClientWrapper.prototype.requestSymbolsForWorkspace = function (params) { + return this._request(ToolingInfo.FEATURES.PROJECT_SYMBOLS, params); + }; + + //These will mostly be callbacks/[done-fail](promises) + /** + Window OnNotifications + */ + //showMessage + LanguageClientWrapper.prototype.addOnShowMessage = function (handler) { + this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.SHOW_MESSAGE, handler); + }; + + //logMessage + LanguageClientWrapper.prototype.addOnLogMessage = function (handler) { + this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.LOG_MESSAGE, handler); + }; + + /** + healthData/logging OnNotifications + */ + //telemetry + LanguageClientWrapper.prototype.addOnTelemetryEvent = function (handler) { + this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.TELEMETRY, handler); + }; + + /** + textDocument OnNotifications + */ + //onPublishDiagnostics + LanguageClientWrapper.prototype.addOnCodeInspection = function (handler) { + this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.DIAGNOSTICS, handler); + }; + + /** + Window OnRequest + */ + + //showMessageRequest - handler must return promise + LanguageClientWrapper.prototype.onShowMessageWithRequest = function (handler) { + this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.SHOW_SELECT_MESSAGE, handler); + }; + + LanguageClientWrapper.prototype.onProjectFoldersRequest = function (handler) { + this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.PROJECT_FOLDERS_REQUEST, handler); + }; + + LanguageClientWrapper.prototype._registrationShim = function (params, handler) { + var self = this; + + var registrations = params.registrations; + registrations.forEach(function (registration) { + var id = registration.id; + self._dynamicCapabilities[id] = registration; + }); + return validateHandler(handler) ? handler(params) : $.Deferred().resolve(); + }; + + LanguageClientWrapper.prototype.onDynamicCapabilityRegistration = function (handler) { + this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST, handler); + }; + + LanguageClientWrapper.prototype._unregistrationShim = function (params, handler) { + var self = this; + + var unregistrations = params.unregistrations; + unregistrations.forEach(function (unregistration) { + var id = unregistration.id; + delete self._dynamicCapabilities[id]; + }); + return validateHandler(handler) ? handler(params) : $.Deferred().resolve(); + }; + + LanguageClientWrapper.prototype.onDynamicCapabilityUnregistration = function (handler) { + this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST, handler); + }; + + /* + Unimplemented OnNotifications + workspace + applyEdit (codeAction, codeLens) + */ + + /** + SendNotifications + */ + + /** + workspace SendNotifications + */ + //didChangeProjectRoots + LanguageClientWrapper.prototype.notifyProjectRootsChanged = function (params) { + this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED, params); + }; + + /** + textDocument SendNotifications + */ + //didOpenTextDocument + LanguageClientWrapper.prototype.notifyTextDocumentOpened = function (params) { + this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED, params); + }; + + //didCloseTextDocument + LanguageClientWrapper.prototype.notifyTextDocumentClosed = function (params) { + this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED, params); + }; + + //didChangeTextDocument + LanguageClientWrapper.prototype.notifyTextDocumentChanged = function (params) { + this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED, params); + }; + + //didSaveTextDocument + LanguageClientWrapper.prototype.notifyTextDocumentSave = function (params) { + this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED, params); + }; + + /** + Custom messages + */ + + //customNotification + LanguageClientWrapper.prototype.sendCustomNotification = function (params) { + this._notify(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION, params); + }; + + LanguageClientWrapper.prototype.onCustomNotification = function (type, handler) { + this._addOnNotificationHandler(type, handler); + }; + + //customRequest + LanguageClientWrapper.prototype.sendCustomRequest = function (params) { + return this._request(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST, params); + }; + + LanguageClientWrapper.prototype.onCustomRequest = function (type, handler) { + this._addOnRequestHandler(type, handler); + }; + + //Handling Brackets Events + LanguageClientWrapper.prototype.addOnEditorChangeHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["activeEditorChange"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addOnProjectOpenHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["projectOpen"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addBeforeProjectCloseHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["beforeProjectClose"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addOnDocumentDirtyFlagChangeHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["dirtyFlagChange"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addOnDocumentChangeHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["documentChange"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addOnFileRenameHandler = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["fileNameChange"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addBeforeAppClose = function (handler) { + if (validateHandler(handler)) { + this._onEventHandlers["beforeAppClose"].push(handler); + } + }; + + LanguageClientWrapper.prototype.addOnCustomEventHandler = function (eventName, handler) { + if (validateHandler(handler)) { + if (!this._onEventHandlers[eventName]) { + this._onEventHandlers[eventName] = []; + } + this._onEventHandlers[eventName].push(handler); + } + }; + + LanguageClientWrapper.prototype.triggerEvent = function (event) { + var eventName = event.type, + eventArgs = arguments; + + if (this._onEventHandlers[eventName] && Array.isArray(this._onEventHandlers[eventName])) { + var handlers = this._onEventHandlers[eventName]; + + handlers.forEach(function (handler) { + if (validateHandler(handler)) { + handler.apply(null, eventArgs); + } + }); + } + }; + + LanguageClientWrapper.prototype.getDynamicCapabilities = function () { + return this._dynamicCapabilities; + }; + + LanguageClientWrapper.prototype.getServerCapabilities = function () { + return this._serverCapabilities; + }; + + LanguageClientWrapper.prototype.setServerCapabilities = function (serverCapabilities) { + this._serverCapabilities = serverCapabilities; + }; + + exports.LanguageClientWrapper = LanguageClientWrapper; + + function logAnalyticsData(typeStrKey) { + var editor = require("editor/EditorManager").getActiveEditor(), + document = editor ? editor.document : null, + language = document ? document.language : null, + languageName = language ? language._name : "", + HealthLogger = require("utils/HealthLogger"), + typeStr = HealthLogger.commonStrings[typeStrKey] || ""; + + HealthLogger.sendAnalyticsData( + HealthLogger.commonStrings.USAGE + HealthLogger.commonStrings.LANGUAGE_SERVER_PROTOCOL + typeStr + languageName, + HealthLogger.commonStrings.USAGE, + HealthLogger.commonStrings.LANGUAGE_SERVER_PROTOCOL, + typeStr, + languageName.toLowerCase() + ); + } + + //For unit testting + exports.validateRequestParams = validateRequestParams; + exports.validateNotificationParams = validateNotificationParams; +}); diff --git a/src/languageTools/LanguageTools.js b/src/languageTools/LanguageTools.js new file mode 100644 index 00000000000..08d2bca6b71 --- /dev/null +++ b/src/languageTools/LanguageTools.js @@ -0,0 +1,119 @@ +/* + * 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 no-console: 0*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint-env es6*/ +define(function (require, exports, module) { + "use strict"; + + var ClientLoader = require("languageTools/ClientLoader"), + EditorManager = require("editor/EditorManager"), + ProjectManager = require("project/ProjectManager"), + DocumentManager = require("document/DocumentManager"), + DocumentModule = require("document/Document"), + PreferencesManager = require("preferences/PreferencesManager"), + Strings = require("strings"), + LanguageClientWrapper = require("languageTools/LanguageClientWrapper").LanguageClientWrapper; + + var languageClients = new Map(), + languageToolsPrefs = { + showServerLogsInConsole: false + }, + BRACKETS_EVENTS_NAMES = { + EDITOR_CHANGE_EVENT: "activeEditorChange", + PROJECT_OPEN_EVENT: "projectOpen", + PROJECT_CLOSE_EVENT: "beforeProjectClose", + DOCUMENT_DIRTY_EVENT: "dirtyFlagChange", + DOCUMENT_CHANGE_EVENT: "documentChange", + FILE_RENAME_EVENT: "fileNameChange", + BEFORE_APP_CLOSE: "beforeAppClose" + }; + + PreferencesManager.definePreference("languageTools", "object", languageToolsPrefs, { + description: Strings.LANGUAGE_TOOLS_PREFERENCES + }); + + PreferencesManager.on("change", "languageTools", function () { + languageToolsPrefs = PreferencesManager.get("languageTools"); + + ClientLoader.syncPrefsWithDomain(languageToolsPrefs); + }); + + function registerLanguageClient(clientName, languageClient) { + languageClients.set(clientName, languageClient); + } + + function _withNamespace(event) { + return event.split(" ") + .filter((value) => !!value) + .map((value) => value + ".language-tools") + .join(" "); + } + + function _eventHandler() { + var eventArgs = arguments; + //Broadcast event to all clients + languageClients.forEach(function (client) { + client.triggerEvent.apply(client, eventArgs); + }); + } + + function _attachEventHandlers() { + //Attach standard listeners + EditorManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.EDITOR_CHANGE_EVENT), _eventHandler); //(event, current, previous) + ProjectManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.PROJECT_OPEN_EVENT), _eventHandler); //(event, directory) + ProjectManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.PROJECT_CLOSE_EVENT), _eventHandler); //(event, directory) + DocumentManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.DOCUMENT_DIRTY_EVENT), _eventHandler); //(event, document) + DocumentModule.on(_withNamespace(BRACKETS_EVENTS_NAMES.DOCUMENT_CHANGE_EVENT), _eventHandler); //(event, document, changeList) + DocumentManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.FILE_RENAME_EVENT), _eventHandler); //(event, oldName, newName) + ProjectManager.on(_withNamespace(BRACKETS_EVENTS_NAMES.BEFORE_APP_CLOSE), _eventHandler); //(event, oldName, newName) + } + + _attachEventHandlers(); + + function listenToCustomEvent(eventModule, eventName) { + eventModule.on(_withNamespace(eventName), _eventHandler); + } + + function initiateToolingService(clientName, clientFilePath, languages) { + var result = $.Deferred(); + + ClientLoader.initiateLanguageClient(clientName, clientFilePath) + .done(function (languageClientInfo) { + var languageClientName = languageClientInfo.name, + languageClientInterface = languageClientInfo.interface, + languageClient = new LanguageClientWrapper(languageClientName, clientFilePath, languageClientInterface, languages); + + registerLanguageClient(languageClientName, languageClient); + + result.resolve(languageClient); + }) + .fail(result.reject); + + return result; + } + + exports.initiateToolingService = initiateToolingService; + exports.listenToCustomEvent = listenToCustomEvent; +}); diff --git a/src/languageTools/PathConverters.js b/src/languageTools/PathConverters.js new file mode 100644 index 00000000000..8663a5478c2 --- /dev/null +++ b/src/languageTools/PathConverters.js @@ -0,0 +1,82 @@ +/* + * 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 PathUtils = require("thirdparty/path-utils/path-utils"), + FileUtils = require("file/FileUtils"); + + function uriToPath(uri) { + var url = PathUtils.parseUrl(uri); + if (url.protocol !== 'file:' || url.pathname === undefined) { + return uri; + } + + let filePath = decodeURIComponent(url.pathname); + if (brackets.platform === 'win') { + if (filePath && filePath.includes(":/") && filePath[0] === '/') { + filePath = filePath.substr(1); + } + return filePath; + } + return filePath; + } + + function pathToUri(filePath) { + var newPath = convertWinToPosixPath(filePath); + if (newPath[0] !== '/') { + newPath = `/${newPath}`; + } + return encodeURI(`file://${newPath}`).replace(/[?#]/g, encodeURIComponent); + } + + function convertToWorkspaceFolders(paths) { + var workspaceFolders = paths.map(function (folderPath) { + var uri = pathToUri(folderPath), + name = FileUtils.getBasename(folderPath); + + return { + uri: uri, + name: name + }; + }); + + return workspaceFolders; + } + + function convertPosixToWinPath(path) { + return path.replace(/\//g, '\\'); + } + + function convertWinToPosixPath(path) { + return path.replace(/\\/g, '/'); + } + + exports.uriToPath = uriToPath; + exports.pathToUri = pathToUri; + exports.convertPosixToWinPath = convertPosixToWinPath; + exports.convertPosixToWinPath = convertPosixToWinPath; + exports.convertToWorkspaceFolders = convertToWorkspaceFolders; +}); diff --git a/src/languageTools/ToolingInfo.json b/src/languageTools/ToolingInfo.json new file mode 100644 index 00000000000..d7457ec6a9e --- /dev/null +++ b/src/languageTools/ToolingInfo.json @@ -0,0 +1,41 @@ +{ + "LANGUAGE_SERVICE": { + "START": "start", + "STOP": "stop", + "REQUEST": "request", + "NOTIFY": "notify", + "CANCEL_REQUEST": "cancelRequest", + "CUSTOM_REQUEST": "customRequest", + "CUSTOM_NOTIFICATION": "customNotification" + }, + "SERVICE_NOTIFICATIONS": { + "SHOW_MESSAGE": "showMessage", + "LOG_MESSAGE": "logMessage", + "TELEMETRY": "telemetry", + "DIAGNOSTICS": "diagnostics" + }, + "SERVICE_REQUESTS": { + "SHOW_SELECT_MESSAGE": "showSelectMessage", + "REGISTRATION_REQUEST": "registerDynamicCapability", + "UNREGISTRATION_REQUEST": "unregisterDynamicCapability", + "PROJECT_FOLDERS_REQUEST": "projectFoldersRequest" + }, + "SYNCHRONIZE_EVENTS": { + "DOCUMENT_OPENED": "didOpen", + "DOCUMENT_CHANGED": "didChange", + "DOCUMENT_SAVED": "didSave", + "DOCUMENT_CLOSED": "didClose", + "PROJECT_FOLDERS_CHANGED": "projectRootsChanged" + }, + "FEATURES": { + "CODE_HINTS": "codehints", + "CODE_HINT_INFO": "hintInfo", + "PARAMETER_HINTS": "parameterHints", + "JUMP_TO_DECLARATION": "declaration", + "JUMP_TO_DEFINITION": "definition", + "JUMP_TO_IMPL": "implementation", + "FIND_REFERENCES": "references", + "DOCUMENT_SYMBOLS": "documentSymbols", + "PROJECT_SYMBOLS": "projectSymbols" + } +} diff --git a/src/languageTools/node/RegisterLanguageClientInfo.js b/src/languageTools/node/RegisterLanguageClientInfo.js new file mode 100644 index 00000000000..01b65fb7f1b --- /dev/null +++ b/src/languageTools/node/RegisterLanguageClientInfo.js @@ -0,0 +1,296 @@ +/* + * 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*/ +/*eslint-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +"use strict"; + +var domainName = "LanguageClientInfo", + LANGUAGE_CLIENT_RELATIVE_PATH_ARRAY = ["languageTools", "LanguageClient", "LanguageClient"], + FORWARD_SLASH = "/", + BACKWARD_SLASH = "\\", + CompletionItemKind = { + Text: 1, + Method: 2, + Function: 3, + Constructor: 4, + Field: 5, + Variable: 6, + Class: 7, + Interface: 8, + Module: 9, + Property: 10, + Unit: 11, + Value: 12, + Enum: 13, + Keyword: 14, + Snippet: 15, + Color: 16, + File: 17, + Reference: 18, + Folder: 19, + EnumMember: 20, + Constant: 21, + Struct: 22, + Event: 23, + Operator: 24, + TypeParameter: 25 + }, + SymbolKind = { + File: 1, + Module: 2, + Namespace: 3, + Package: 4, + Class: 5, + Method: 6, + Property: 7, + Field: 8, + Constructor: 9, + Enum: 10, + Interface: 11, + Function: 12, + Variable: 13, + Constant: 14, + String: 15, + Number: 16, + Boolean: 17, + Array: 18, + Object: 19, + Key: 20, + Null: 21, + EnumMember: 22, + Struct: 23, + Event: 24, + Operator: 25, + TypeParameter: 26 + }, + defaultBracketsCapabilities = { + //brackets default capabilties + "workspace": { + "workspaceFolders": true, + "symbol": { + "dynamicRegistration": false, + "symbolKind": [ + SymbolKind.File, + SymbolKind.Module, + SymbolKind.Namespace, + SymbolKind.Package, + SymbolKind.Class, + SymbolKind.Method, + SymbolKind.Property, + SymbolKind.Field, + SymbolKind.Constructor, + SymbolKind.Enum, + SymbolKind.Interface, + SymbolKind.Function, + SymbolKind.Variable, + SymbolKind.Constant, + SymbolKind.String, + SymbolKind.Number, + SymbolKind.Boolean, + SymbolKind.Array, + SymbolKind.Object, + SymbolKind.Key, + SymbolKind.Null, + SymbolKind.EnumMember, + SymbolKind.Struct, + SymbolKind.Event, + SymbolKind.Operator, + SymbolKind.TypeParameter + ] + } + }, + "textDocument": { + "synchronization": { + "didSave": true + }, + "completion": { + "dynamicRegistration": false, + "completionItem": { + "deprecatedSupport": true, + "documentationFormat": ["plaintext"], + "preselectSupport": true + }, + "completionItemKind": { + "valueSet": [ + CompletionItemKind.Text, + CompletionItemKind.Method, + CompletionItemKind.Function, + CompletionItemKind.Constructor, + CompletionItemKind.Field, + CompletionItemKind.Variable, + CompletionItemKind.Class, + CompletionItemKind.Interface, + CompletionItemKind.Module, + CompletionItemKind.Property, + CompletionItemKind.Unit, + CompletionItemKind.Value, + CompletionItemKind.Enum, + CompletionItemKind.Keyword, + CompletionItemKind.Snippet, + CompletionItemKind.Color, + CompletionItemKind.File, + CompletionItemKind.Reference, + CompletionItemKind.Folder, + CompletionItemKind.EnumMember, + CompletionItemKind.Constant, + CompletionItemKind.Struct, + CompletionItemKind.Event, + CompletionItemKind.Operator, + CompletionItemKind.TypeParameter + ] + }, + "contextSupport": true + }, + "signatureHelp": { + "dynamicRegistration": false, + "signatureInformation": { + "documentationFormat": ["plaintext"] + } + }, + "references": { + "dynamicRegistration": false + }, + "documentSymbol": { + "dynamicRegistration": false, + "symbolKind": { + "valueSet": [ + SymbolKind.File, + SymbolKind.Module, + SymbolKind.Namespace, + SymbolKind.Package, + SymbolKind.Class, + SymbolKind.Method, + SymbolKind.Property, + SymbolKind.Field, + SymbolKind.Constructor, + SymbolKind.Enum, + SymbolKind.Interface, + SymbolKind.Function, + SymbolKind.Variable, + SymbolKind.Constant, + SymbolKind.String, + SymbolKind.Number, + SymbolKind.Boolean, + SymbolKind.Array, + SymbolKind.Object, + SymbolKind.Key, + SymbolKind.Null, + SymbolKind.EnumMember, + SymbolKind.Struct, + SymbolKind.Event, + SymbolKind.Operator, + SymbolKind.TypeParameter + ] + }, + "hierarchicalDocumentSymbolSupport": false + }, + "definition": { + "dynamicRegistration": false + }, + "declaration": { + "dynamicRegistration": false + }, + "typeDefinition": { + "dynamicRegistration": false + }, + "implementation": { + "dynamicRegistration": false + }, + "publishDiagnostics": { + "relatedInformation": true + } + } + }; + +function syncPreferences(prefs) { + global.LanguageClientInfo = global.LanguageClientInfo || {}; + global.LanguageClientInfo.preferences = prefs || global.LanguageClientInfo.preferences || {}; +} + +function initialize(bracketsSourcePath, toolingInfo, resolve) { + if (!bracketsSourcePath || !toolingInfo) { + resolve(true, null); //resolve with err param + } + + var normalizedBracketsSourcePath = bracketsSourcePath.split(BACKWARD_SLASH).join(FORWARD_SLASH), + bracketsSourcePathArray = normalizedBracketsSourcePath.split(FORWARD_SLASH), + languageClientAbsolutePath = bracketsSourcePathArray.concat(LANGUAGE_CLIENT_RELATIVE_PATH_ARRAY).join(FORWARD_SLASH); + + global.LanguageClientInfo = global.LanguageClientInfo || {}; + global.LanguageClientInfo.languageClientPath = languageClientAbsolutePath; + global.LanguageClientInfo.defaultBracketsCapabilities = defaultBracketsCapabilities; + global.LanguageClientInfo.toolingInfo = toolingInfo; + global.LanguageClientInfo.preferences = {}; + + resolve(null, true); //resolve with boolean denoting success +} + +function init(domainManager) { + if (!domainManager.hasDomain(domainName)) { + domainManager.registerDomain(domainName, { + major: 0, + minor: 1 + }); + } + + domainManager.registerCommand( + domainName, + "initialize", + initialize, + true, + "Initialize node environment for Language Client Module", + [ + { + name: "bracketsSourcePath", + type: "string", + description: "Absolute path to the brackets source" + }, + { + name: "toolingInfo", + type: "object", + description: "Tooling Info json to be used by Language Client" + } + ], + [] + ); + + domainManager.registerCommand( + domainName, + "syncPreferences", + syncPreferences, + false, + "Sync language tools preferences for Language Client Module", + [ + { + name: "prefs", + type: "object", + description: "Language tools preferences" + } + ], + [] + ); +} + +exports.init = init; diff --git a/src/languageTools/styles/default_provider_style.css b/src/languageTools/styles/default_provider_style.css new file mode 100644 index 00000000000..5090330b28c --- /dev/null +++ b/src/languageTools/styles/default_provider_style.css @@ -0,0 +1,134 @@ +/* + * 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. + * + */ + + +span.brackets-hints-with-type-details { + width: 300px; + display: inline-block; +} + +.brackets-hints-type-details { + color: #a3a3a3 !important; + font-weight: 100; + font-style: italic !important; + margin-right: 5px; + float: right; +} + +.hint-description { + display: none; + color: #d4d4d4; + word-wrap: break-word; + white-space: normal; + box-sizing: border-box; +} + +.hint-doc { + display: none; + padding-right: 10px !important; + color: grey; + word-wrap: break-word; + white-space: normal; + box-sizing: border-box; + float: left; + clear: left; + max-height: 2em; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1em; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.highlight .hint-description { + display: block; + color: #6495ed !important; +} + +.highlight .hint-doc { + display: -webkit-box; +} + +.dark .brackets-hints-type-details { + color: #696969 !important; +} + +.highlight .brackets-hints-type-details { + display: none; +} + +.brackets-hints-keyword { + font-weight: 100; + font-style: italic !important; + margin-right: 5px; + float: right; + color: #6495ed !important; +} + +.brackets-hints .matched-hint { + font-weight: 500; +} + +#function-hint-container-new { + display: none; + + background: #fff; + position: absolute; + z-index: 15; + left: 400px; + top: 40px; + height: auto; + width: auto; + overflow: scroll; + + padding: 1px 6px; + text-align: center; + + border-radius: 3px; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.24); +} + +#function-hint-container-new .function-hint-content-new { + text-align: left; +} + +.brackets-hints .current-parameter { + font-weight: 500; +} + +/* Dark Styles */ + +.dark #function-hint-container-new { + background: #000; + border: 1px solid rgba(255, 255, 255, 0.15); + color: #fff; + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.24); +} + +.dark .hint-doc { + color: #ccc; +} + +.dark .hint-description { + color: #ccc; +} diff --git a/src/nls/fr/strings.js b/src/nls/fr/strings.js index bb199373bcc..866b74cba2f 100644 --- a/src/nls/fr/strings.js +++ b/src/nls/fr/strings.js @@ -419,6 +419,7 @@ define({ "CMD_QUICK_OPEN": "Ouverture rapide", "CMD_GOTO_LINE": "Atteindre la ligne", "CMD_GOTO_DEFINITION": "Accès rapide à la définition", + "CMD_GOTO_DEFINITION_PROJECT": "Accès rapide à la définition dans le projet", "CMD_GOTO_FIRST_PROBLEM": "Accéder au premier problème", "CMD_TOGGLE_QUICK_EDIT": "Édition rapide", "CMD_TOGGLE_QUICK_DOCS": "Documentation rapide", @@ -650,10 +651,10 @@ define({ // extensions/default/HealthData "HEALTH_DATA_NOTIFICATION": "Health Report Preferences", "HEALTH_FIRST_POPUP_TITLE": "Rapport d’intégrité de {APP_NAME}", - "HEALTH_DATA_DO_TRACK": "Partager des informations anonymes sur la façon dont j’utilise {APP_NAME}", - "HEALTH_DATA_NOTIFICATION_MESSAGE": "

    Afin d’améliorer {APP_NAME}, nous transmettons régulièrement des statistiques limitées et anonymes à Adobe sur la manière dont vous utilisez {APP_NAME}. Ces données permettent de hiérarchiser les fonctionnalités à traiter, de détecter les bugs éventuels et d’identifier les problèmes d’utilisation.

    Pour voir les renseignements collectés et choisir ceux que vous ne souhaitez pas partager, cliquez sur Aide > Rapport d’intégrité.

    Lisez cet article pour en savoir plus concernant le rapport d’intégrité de {APP_NAME}", + "HEALTH_DATA_DO_TRACK": "Partager des informations pseudonymes sur la façon dont j’utilise {APP_NAME}", + "HEALTH_DATA_NOTIFICATION_MESSAGE": "Afin d’améliorer {APP_NAME}, nous transmettons régulièrement des statistiques limitées et pseudonymes à Adobe sur la manière dont vous utilisez {APP_NAME}. Ces données permettent de hiérarchiser les fonctionnalités à traiter, de détecter les bugs éventuels et d’identifier les problèmes d’utilisation.

    Pour voir les renseignements collectés et choisir ceux que vous ne souhaitez pas partager, cliquez sur Aide > Rapport d’intégrité.

    Lisez cet article pour en savoir plus concernant le rapport d’intégrité de {APP_NAME}", "HEALTH_DATA_PREVIEW": "Rapport d’intégrité de {APP_NAME}", - "HEALTH_DATA_PREVIEW_INTRO": "

    Afin d’améliorer {APP_NAME}, nous transmettons régulièrement des statistiques limitées et anonymes à Adobe sur la manière dont vous utilisez {APP_NAME}. Ces données permettent de hiérarchiser les fonctionnalités à traiter, de détecter les bugs éventuels et d’identifier les problèmes d’utilisation. Lisez cet article concernant le rapport d’intégrité de {APP_NAME} et découvrez en quoi il est utile à la communauté {APP_NAME} tout en préservant votre confidentialité.

    Vous trouverez ci-dessous un résumé des données qui seront envoyées dans le cadre de votre prochain rapport d’intégrité si vous décidez d’activer cette option.

    ", + "HEALTH_DATA_PREVIEW_INTRO": "

    Afin d’améliorer {APP_NAME}, nous transmettons régulièrement des statistiques limitées et pseudonymes à Adobe sur la manière dont vous utilisez {APP_NAME}. Ces données permettent de hiérarchiser les fonctionnalités à traiter, de détecter les bugs éventuels et d’identifier les problèmes d’utilisation. Lisez cet article concernant le rapport d’intégrité de {APP_NAME} et découvrez en quoi il est utile à la communauté {APP_NAME} tout en préservant votre confidentialité.

    Vous trouverez ci-dessous un résumé des données qui seront envoyées dans le cadre de votre prochain rapport d’intégrité si vous décidez d’activer cette option.

    ", // extensions/default/InlineTimingFunctionEditor "INLINE_TIMING_EDITOR_TIME": "Temps", @@ -872,8 +873,29 @@ define({ "DESCRIPTION_AUTO_UPDATE": "Activer/désactiver la mise à jour automatique de Brackets", "AUTOUPDATE_ERROR": "Erreur !", "AUTOUPDATE_IN_PROGRESS": "Une mise à jour est déjà en cours.", - - "NUMBER_WITH_PERCENTAGE": "{0}%", + + "NUMBER_WITH_PERCENTAGE": "{0} %", // Strings for Related Files - "CMD_FIND_RELATED_FILES": "Trouver les fichiers associés" + "CMD_FIND_RELATED_FILES": "Trouver les fichiers associés", + + ///String for Php Tooling Extensions + "PHP_VERSION_INVALID": "Erreur lors de l’analyse de la version de PHP. Veuillez vérifier la sortie de la commande « php –version ».", + "PHP_UNSUPPORTED_VERSION": "Installez le moteur d’exécution de PHP 7 pour activer les outils correspondants comme Conseils de code, Conseils de paramètres, Accéder à la définition, etc. Version trouvée : {0}", + "PHP_EXECUTABLE_NOT_FOUND": "Moteur d’exécution PHP introuvable. Installez le moteur d’exécution de PHP 7 et mettez à jour « executablePath » dans les préférences PHP. Cela permettra l’activation des outils liés à PHP comme Conseils de code, Conseils de paramètres ou encore Accéder à la définition.", + "PHP_PROCESS_SPAWN_ERROR": "Code d’erreur {0} rencontré lors du démarrage du processus PHP.", + "PHP_SERVER_ERROR_TITLE": "Erreur", + "PHP_SERVER_MEMORY_LIMIT_INVALID": "La limite de mémoire que vous avez fournie n’est pas valide. Veuillez corriger la valeur indiquée dans les préférences PHP.", + "DESCRIPTION_PHP_TOOLING_CONFIGURATION": "Paramètres de configuration par défaut des outils PHP", + "OPEN_PREFERENNCES": "Ouvrir les préférences", + + //Strings for LanguageTools Preferences + "LANGUAGE_TOOLS_PREFERENCES": "Préférences pour les outils linguistiques", + + "FIND_ALL_REFERENCES": "Rechercher toutes les références", + "REFERENCES_IN_FILES": "références", + "REFERENCE_IN_FILES": "référence", + "REFERENCES_NO_RESULTS": "Références non disponibles pour la position actuelle du curseur", + + "CMD_FIND_DOCUMENT_SYMBOLS": "Rechercher des symboles de document", + "CMD_FIND_PROJECT_SYMBOLS": "Rechercher des symboles de projet" }); diff --git a/src/nls/ja/strings.js b/src/nls/ja/strings.js index 85428d85fcb..5a2a88733c7 100644 --- a/src/nls/ja/strings.js +++ b/src/nls/ja/strings.js @@ -419,6 +419,7 @@ define({ "CMD_QUICK_OPEN": "クイックオープン", "CMD_GOTO_LINE": "行に移動", "CMD_GOTO_DEFINITION": "定義をクイック検索", + "CMD_GOTO_DEFINITION_PROJECT": "プロジェクトで定義をクイック検索", "CMD_GOTO_FIRST_PROBLEM": "最初の問題に移動", "CMD_TOGGLE_QUICK_EDIT": "クイック編集", "CMD_TOGGLE_QUICK_DOCS": "クイックドキュメント", @@ -650,10 +651,10 @@ define({ // extensions/default/HealthData "HEALTH_DATA_NOTIFICATION": "Health Report Preferences", "HEALTH_FIRST_POPUP_TITLE": "{APP_NAME} の正常性レポート", - "HEALTH_DATA_DO_TRACK": "{APP_NAME} の使用方法に関する情報を匿名で共有します", - "HEALTH_DATA_NOTIFICATION_MESSAGE": "{APP_NAME} 品質向上のため、アドビでは、お客様の {APP_NAME} の使用方法に関する限られた匿名の統計をアドビに定期的に送信しています。この情報は、機能を優先順位付けし、バグを発見し、操作性の問題を検出する際に役立ちます。

    お客様のデータを確認するには、または、データを共有しないように選択するには、ヘルプ/正常性レポートを選択してください。

    {APP_NAME} の正常性レポートに関する詳細情報", + "HEALTH_DATA_DO_TRACK": "{APP_NAME} の使用方法に関する偽名情報を共有します", + "HEALTH_DATA_NOTIFICATION_MESSAGE": "{APP_NAME} 品質向上のため、アドビでは、お客様の {APP_NAME} の使用方法に関する限られた偽名の統計をアドビに定期的に送信しています。この情報は、機能を優先順位付けし、バグを発見し、操作性の問題を検出する際に役立ちます。

    ヘルプ/正常性レポートから、お客様のデータの確認またはデータの非共有の選択をすることができます。

    {APP_NAME} の正常性レポートに関する詳細情報", "HEALTH_DATA_PREVIEW": "{APP_NAME} の正常性レポート", - "HEALTH_DATA_PREVIEW_INTRO": "

    {APP_NAME} 品質向上のため、アドビでは、お客様の {APP_NAME} の使用方法に関する限られた匿名の統計をアドビに定期的に送信しています。この情報は、機能を優先順位付けし、バグを発見し、操作性の問題を検出する際に役立ちます。{APP_NAME} の正常性レポートについて、またこれが {APP_NAME} コミュニティにどのように役立ち、プライバシーを保護するかついて詳細をご確認ください。

    有効にした場合に、次回のお客様の正常性レポートで送信されるデータのプレビューを以下に示します。

    ", + "HEALTH_DATA_PREVIEW_INTRO": "

    {APP_NAME} 品質向上のため、アドビでは、お客様の {APP_NAME} の使用方法に関する限られた偽名の統計をアドビに定期的に送信しています。この情報は、機能を優先順位付けし、バグを発見し、操作性の問題を検出する際に役立ちます。{APP_NAME} の正常性レポートについての詳細およびレポートがお客様のプライバシーを保護した上でどのように {APP_NAME} コミュニティに役立つかをご確認ください。

    有効にした場合に、次回のお客様の正常性レポートで送信されるデータのプレビューを以下に示します。

    ", // extensions/default/InlineTimingFunctionEditor "INLINE_TIMING_EDITOR_TIME": "時間", @@ -872,8 +873,29 @@ define({ "DESCRIPTION_AUTO_UPDATE": "Brackets の自動更新を有効化/無効化", "AUTOUPDATE_ERROR": "エラー!", "AUTOUPDATE_IN_PROGRESS": "更新は既に進行中です。", - + "NUMBER_WITH_PERCENTAGE": "{0}%", // Strings for Related Files - "CMD_FIND_RELATED_FILES": "関連するファイルを検索" + "CMD_FIND_RELATED_FILES": "関連するファイルを検索", + + ///String for Php Tooling Extensions + "PHP_VERSION_INVALID": "PHP バージョンを解析する際のエラーです。“php –version” コマンドの出力を確認してください。", + "PHP_UNSUPPORTED_VERSION": "コードヒント、パラメーターヒント、定義にジャンプなどの PHP 関連のツールを有効化するために、PHP7 ランタイムをインストールしてください。検出されたバージョン: {0}", + "PHP_EXECUTABLE_NOT_FOUND": "PHP ランタイムが見つかりません。PHP7 ランタイムをインストールして、PHP の環境設定で適切に “executablePath” を更新してください。これにより、コードヒント、パラメーターヒント、定義にジャンプなどの PHP 関連のツールが有効になります。", + "PHP_PROCESS_SPAWN_ERROR": "PHP プロセスを起動中に、エラーコード {0} が発生しました。", + "PHP_SERVER_ERROR_TITLE": "エラー", + "PHP_SERVER_MEMORY_LIMIT_INVALID": "指定したメモリ制限は無効です。正しい値を設定するために、PHP の環境設定を確認してください。", + "DESCRIPTION_PHP_TOOLING_CONFIGURATION": "PHP ツールのデフォルト設定", + "OPEN_PREFERENNCES": "環境設定を開く", + + //Strings for LanguageTools Preferences + "LANGUAGE_TOOLS_PREFERENCES": "言語ツールの設定", + + "FIND_ALL_REFERENCES": "すべての参照を検索", + "REFERENCES_IN_FILES": "参照", + "REFERENCE_IN_FILES": "参照", + "REFERENCES_NO_RESULTS": "現在のカーソル位置で利用可能な参照はありません", + + "CMD_FIND_DOCUMENT_SYMBOLS": "ドキュメント記号を検索", + "CMD_FIND_PROJECT_SYMBOLS": "プロジェクト記号を検索" }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index a442bfdf9b9..dc74e2f354c 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -419,6 +419,7 @@ define({ "CMD_QUICK_OPEN" : "Quick Open", "CMD_GOTO_LINE" : "Go to Line", "CMD_GOTO_DEFINITION" : "Quick Find Definition", + "CMD_GOTO_DEFINITION_PROJECT" : "Quick Find Definition in Project", "CMD_GOTO_FIRST_PROBLEM" : "Go to First Problem", "CMD_TOGGLE_QUICK_EDIT" : "Quick Edit", "CMD_TOGGLE_QUICK_DOCS" : "Quick Docs", @@ -650,10 +651,10 @@ define({ // extensions/default/HealthData "HEALTH_DATA_NOTIFICATION" : "Health Report Preferences", "HEALTH_FIRST_POPUP_TITLE" : "{APP_NAME} Health Report", - "HEALTH_DATA_DO_TRACK" : "Share anonymous information on how I use {APP_NAME}", - "HEALTH_DATA_NOTIFICATION_MESSAGE" : "In order to improve {APP_NAME}, we periodically send limited, anonymous statistics to Adobe about how you use {APP_NAME}. This information helps prioritize features, find bugs, and spot usability issues.

    You can see your data or choose not to share data by selecting Help > Health Report.

    Learn more about {APP_NAME} Health Report", + "HEALTH_DATA_DO_TRACK" : "Share pseudonymous information on how I use {APP_NAME}", + "HEALTH_DATA_NOTIFICATION_MESSAGE" : "In order to improve {APP_NAME}, we periodically send limited, pseudonymous statistics to Adobe about how you use {APP_NAME}. This information helps prioritize features, find bugs, and spot usability issues.

    You can see your data or choose not to share data by selecting Help > Health Report.

    Learn more about {APP_NAME} Health Report", "HEALTH_DATA_PREVIEW" : "{APP_NAME} Health Report", - "HEALTH_DATA_PREVIEW_INTRO" : "

    In order to improve {APP_NAME}, we periodically send limited, anonymous statistics to Adobe about how you use {APP_NAME}. This information helps prioritize features, find bugs, and spot usability issues. Learn more about {APP_NAME} Health Report and how it benefits the {APP_NAME} community while protecting your privacy.

    Below is a preview of the data that will be sent in your next Health Report if it is enabled.

    ", + "HEALTH_DATA_PREVIEW_INTRO" : "

    In order to improve {APP_NAME}, we periodically send limited, pseudonymous statistics to Adobe about how you use {APP_NAME}. This information helps prioritize features, find bugs, and spot usability issues. Learn more about {APP_NAME} Health Report and how it benefits the {APP_NAME} community while protecting your privacy.

    Below is a preview of the data that will be sent in your next Health Report if it is enabled.

    ", // extensions/default/InlineTimingFunctionEditor "INLINE_TIMING_EDITOR_TIME" : "Time", @@ -689,7 +690,7 @@ define({ "ERROR_EXTRACTTO_VARIABLE_MULTICURSORS" : "Extract to Variable does not work in multicursors", "ERROR_EXTRACTTO_FUNCTION_MULTICURSORS" : "Extract to Function does not work in multicursors", "EXTRACTTO_FUNCTION_SELECT_SCOPE" : "Choose destination scope", - "EXTRACTTO_VARIABLE_SELECT_EXPRESSION" : "Select a expression", + "EXTRACTTO_VARIABLE_SELECT_EXPRESSION" : "Select an expression", "CMD_REFACTORING_RENAME" : "Rename", "CMD_REFACTORING_TRY_CATCH" : "Wrap in Try Catch", "CMD_REFACTORING_CONDITION" : "Wrap in Condition", @@ -872,8 +873,29 @@ define({ "DESCRIPTION_AUTO_UPDATE" : "Enable/disable Brackets Auto-update", "AUTOUPDATE_ERROR" : "Error!", "AUTOUPDATE_IN_PROGRESS" : "An update is already in progress.", - + "NUMBER_WITH_PERCENTAGE" : "{0}%", // Strings for Related Files - "CMD_FIND_RELATED_FILES" : "Find Related Files" + "CMD_FIND_RELATED_FILES" : "Find Related Files", + + ///String for Php Tooling Extensions + "PHP_VERSION_INVALID" : "Error parsing PHP version. Please check the output of the “php –version” command.", + "PHP_UNSUPPORTED_VERSION" : "Install PHP7 runtime for enabling PHP-related tooling such as Code Hints, Parameter Hints, Jump To Definition and more. Version found: {0}", + "PHP_EXECUTABLE_NOT_FOUND" : "PHP runtime not found. Install the PHP7 runtime and update “executablePath” in PHP Preferences appropriately. This enables PHP-related tooling such as Code Hints, Parameter Hints, Jump To Definition and more.", + "PHP_PROCESS_SPAWN_ERROR" : "Error code {0} encountered while starting the PHP process.", + "PHP_SERVER_ERROR_TITLE" : "Error", + "PHP_SERVER_MEMORY_LIMIT_INVALID" : "The memory limit you provided is invalid. Review the PHP preferences to set the correct value.", + "DESCRIPTION_PHP_TOOLING_CONFIGURATION" : "PHP Tooling default configuration settings", + "OPEN_PREFERENNCES" : "Open Preferences", + + //Strings for LanguageTools Preferences + "LANGUAGE_TOOLS_PREFERENCES" : "Preferences for Language Tools", + + "FIND_ALL_REFERENCES" : "Find All References", + "REFERENCES_IN_FILES" : "references", + "REFERENCE_IN_FILES" : "reference", + "REFERENCES_NO_RESULTS" : "No References available for current cursor position", + + "CMD_FIND_DOCUMENT_SYMBOLS" : "Find Document Symbols", + "CMD_FIND_PROJECT_SYMBOLS" : "Find Project Symbols" }); diff --git a/src/search/QuickOpen.js b/src/search/QuickOpen.js index 21255cc5f41..98336ac40a0 100644 --- a/src/search/QuickOpen.js +++ b/src/search/QuickOpen.js @@ -44,8 +44,40 @@ define(function (require, exports, module) { LanguageManager = require("language/LanguageManager"), ModalBar = require("widgets/ModalBar").ModalBar, QuickSearchField = require("search/QuickSearchField").QuickSearchField, - StringMatch = require("utils/StringMatch"); - + StringMatch = require("utils/StringMatch"), + ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler; + + var _providerRegistrationHandler = new ProviderRegistrationHandler(), + _registerQuickOpenProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler); + + var SymbolKind = { + "1": "File", + "2": "Module", + "3": "Namespace", + "4": "Package", + "5": "Class", + "6": "Method", + "7": "Property", + "8": "Field", + "9": "Constructor", + "10": "Enum", + "11": "Interface", + "12": "Function", + "13": "Variable", + "14": "Constant", + "15": "String", + "16": "Number", + "17": "Boolean", + "18": "Array", + "19": "Object", + "20": "Key", + "21": "Null", + "22": "EnumMember", + "23": "Struct", + "24": "Event", + "25": "Operator", + "26": "TypeParameter" + }; /** * The regular expression to check the cursor position @@ -53,12 +85,6 @@ define(function (require, exports, module) { */ var CURSOR_POS_EXP = new RegExp(":([^,]+)?(,(.+)?)?"); - /** - * List of plugins - * @type {Array.} - */ - var plugins = []; - /** * Current plugin * @type {QuickOpenPlugin} @@ -77,6 +103,22 @@ define(function (require, exports, module) { */ var _curDialog; + /** + * Helper function to get the plugins based on the type of the current document. + * @private + * @returns {Array} Returns the plugings based on the languageId of the current document. + */ + function _getPluginsForCurrentContext() { + var curDoc = DocumentManager.getCurrentDocument(); + + if (curDoc) { + var languageId = curDoc.getLanguage().getId(); + return _providerRegistrationHandler.getProvidersForLanguageId(languageId); + } + + return _providerRegistrationHandler.getProvidersForLanguageId(); //plugins registered for all + } + /** * Defines API for new QuickOpen plug-ins */ @@ -132,18 +174,22 @@ define(function (require, exports, module) { * cancels Quick Open (via Esc), those changes are automatically reverted. */ function addQuickOpenPlugin(pluginDef) { - plugins.push(new QuickOpenPlugin( - pluginDef.name, - pluginDef.languageIds, - pluginDef.done, - pluginDef.search, - pluginDef.match, - pluginDef.itemFocus, - pluginDef.itemSelect, - pluginDef.resultsFormatter, - pluginDef.matcherOptions, - pluginDef.label - )); + var quickOpenProvider = new QuickOpenPlugin( + pluginDef.name, + pluginDef.languageIds, + pluginDef.done, + pluginDef.search, + pluginDef.match, + pluginDef.itemFocus, + pluginDef.itemSelect, + pluginDef.resultsFormatter, + pluginDef.matcherOptions, + pluginDef.label + ), + providerLanguageIds = pluginDef.languageIds.length ? pluginDef.languageIds : ["all"], + providerPriority = pluginDef.priority || 0; + + _registerQuickOpenProvider(quickOpenProvider, providerLanguageIds, providerPriority); } /** @@ -350,9 +396,10 @@ define(function (require, exports, module) { this.closePromise = modalBarClosePromise; this.isOpen = false; - var i; + var i, + plugins = _getPluginsForCurrentContext(); for (i = 0; i < plugins.length; i++) { - var plugin = plugins[i]; + var plugin = plugins[i].provider; if (plugin.done) { plugin.done(); } @@ -367,10 +414,10 @@ define(function (require, exports, module) { // completes) since ModalBar has already resized the editor and done its own scroll adjustment before // this event fired - so anything we set here will override the pos that was (re)set by ModalBar. var editor = EditorManager.getCurrentFullEditor(); - if (this._origSelections) { + if (editor && this._origSelections) { editor.setSelections(this._origSelections); } - if (this._origScrollPos) { + if (editor && this._origScrollPos) { editor.setScrollPos(this._origScrollPos.x, this._origScrollPos.y); } } @@ -455,17 +502,11 @@ define(function (require, exports, module) { return { error: null }; } - // Try to invoke a search plugin - var curDoc = DocumentManager.getCurrentDocument(), languageId; - if (curDoc) { - languageId = curDoc.getLanguage().getId(); - } - - var i; + var i, + plugins = _getPluginsForCurrentContext(); for (i = 0; i < plugins.length; i++) { - var plugin = plugins[i]; - var languageIdMatch = plugin.languageIds.length === 0 || plugin.languageIds.indexOf(languageId) !== -1; - if (languageIdMatch && plugin.match(query)) { + var plugin = plugins[i].provider; + if(plugin.match(query)) { currentPlugin = plugin; // Look up the StringMatcher for this plugin. @@ -613,6 +654,9 @@ define(function (require, exports, module) { case "@": dialogLabel = Strings.CMD_GOTO_DEFINITION + "\u2026"; break; + case "#": + dialogLabel = Strings.CMD_GOTO_DEFINITION_PROJECT + "\u2026"; + break; default: dialogLabel = ""; break; @@ -732,18 +776,89 @@ define(function (require, exports, module) { } } + function doDefinitionSearchInProject() { + if (DocumentManager.getCurrentDocument()) { + beginSearch("#", getCurrentEditorSelectedText()); + } + } + + function _canHandleTrigger(trigger, plugins) { + var retval = false; + + plugins.some(function (plugin, index) { + var provider = plugin.provider; + if (provider.match(trigger)) { + retval = true; + return true; + } + }); + + return retval; + } + + function _setMenuItemStateForLanguage(languageId) { + var plugins = _providerRegistrationHandler.getProvidersForLanguageId(languageId); + if (_canHandleTrigger("@", plugins)) { + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION).setEnabled(true); + } else { + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION).setEnabled(false); + } + + if (_canHandleTrigger("#", plugins)) { + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION_PROJECT).setEnabled(true); + } else { + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION_PROJECT).setEnabled(false); + } + } + // Listen for a change of project to invalidate our file list ProjectManager.on("projectOpen", function () { fileList = null; }); + MainViewManager.on("currentFileChange", function (event, newFile, newPaneId, oldFile, oldPaneId) { + if (!newFile) { + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION).setEnabled(false); + CommandManager.get(Commands.NAVIGATE_GOTO_DEFINITION_PROJECT).setEnabled(false); + return; + } + + var newFilePath = newFile.fullPath, + newLanguageId = LanguageManager.getLanguageForPath(newFilePath).getId(); + _setMenuItemStateForLanguage(newLanguageId); + + DocumentManager.getDocumentForPath(newFilePath) + .done(function (newDoc) { + newDoc.on("languageChanged.quickFindDefinition", function () { + var changedLanguageId = LanguageManager.getLanguageForPath(newDoc.file.fullPath).getId(); + _setMenuItemStateForLanguage(changedLanguageId); + }); + }).fail(function (err) { + console.error(err); + }); + + if (!oldFile) { + return; + } + + var oldFilePath = oldFile.fullPath; + DocumentManager.getDocumentForPath(oldFilePath) + .done(function (oldDoc) { + oldDoc.off("languageChanged.quickFindDefinition"); + }).fail(function (err) { + console.error(err); + }); + }); + CommandManager.register(Strings.CMD_QUICK_OPEN, Commands.NAVIGATE_QUICK_OPEN, doFileSearch); CommandManager.register(Strings.CMD_GOTO_DEFINITION, Commands.NAVIGATE_GOTO_DEFINITION, doDefinitionSearch); + CommandManager.register(Strings.CMD_GOTO_DEFINITION_PROJECT, Commands.NAVIGATE_GOTO_DEFINITION_PROJECT, doDefinitionSearchInProject); CommandManager.register(Strings.CMD_GOTO_LINE, Commands.NAVIGATE_GOTO_LINE, doGotoLine); exports.beginSearch = beginSearch; exports.addQuickOpenPlugin = addQuickOpenPlugin; exports.highlightMatch = highlightMatch; + exports.SymbolKind = SymbolKind; // Convenience exports for functions that most QuickOpen plugins would need. exports.stringMatch = StringMatch.stringMatch; diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js index 13eb92790a0..1822926a26b 100644 --- a/src/search/SearchResultsView.js +++ b/src/search/SearchResultsView.js @@ -72,14 +72,16 @@ define(function (require, exports, module) { * @param {SearchModel} model The model that this view is showing. * @param {string} panelID The CSS ID to use for the panel. * @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). + * @param {string} type type to identify if it is reference search or string match serach */ - function SearchResultsView(model, panelID, panelName) { + function SearchResultsView(model, panelID, panelName, type) { var panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100); this._$summary = this._panel.$panel.find(".title"); this._$table = this._panel.$panel.find(".table-container"); this._model = model; + this._searchResultsType = type; } EventDispatcher.makeEventDispatcher(SearchResultsView.prototype); @@ -116,6 +118,9 @@ define(function (require, exports, module) { /** @type {number} The ID we use for timeouts when handling model changes. */ SearchResultsView.prototype._timeoutID = null; + /** @type {string} The Id we use to check if it is reference search or match search */ + SearchResultsView.prototype._searchResultsType = null; + /** * @private * Handles when model changes. Updates the view, buffering changes if necessary so as not to churn too much. @@ -344,9 +349,14 @@ define(function (require, exports, module) { SearchResultsView.prototype._showSummary = function () { var count = this._model.countFilesMatches(), lastIndex = this._getLastIndex(count.matches), + typeStr = (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, filesStr, summary; + if(this._searchResultsType === "reference") { + typeStr = (count.matches > 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES; + } + filesStr = StringUtils.format( Strings.FIND_NUM_FILES, count.files, @@ -358,7 +368,7 @@ define(function (require, exports, module) { Strings.FIND_TITLE_SUMMARY, this._model.exceedsMaximum ? Strings.FIND_IN_FILES_MORE_THAN : "", String(count.matches), - (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, + typeStr, filesStr ); diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index 3824aa242b2..f6290ce0c0f 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -124,7 +124,10 @@ @button-icon: "images/find-replace-sprites.svg"; @jstree-sprite: url("images/jsTreeSprites.svg") !important; - +// Codehint description +@bc-codehint-desc: #e6e6e6; +@bc-codehint-desc-type-details: #1473e6; +@bc-codehint-desc-documentation:#424242; /* Dark Core UI variables -----------------------------------------------------------------------------*/ @@ -213,3 +216,8 @@ // images @dark-button-icon: "images/find-replace-sprites-dark.svg"; @dark-jstree-sprite: url("images/jsTreeSprites-dark.svg") !important; + +// Codehint description +@dark-bc-codehint-desc: #2c2c2c; +@dark-bc-codehint-desc-type-details: #46a0f5; +@dark-bc-codehint-desc-documentation:#b1b1b1; diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 44c2879a7a6..6ce6c1aee15 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -584,6 +584,47 @@ a:focus { transition: right 167ms, left 167ms; } +#codehint-desc { + background: @bc-codehint-desc; + position: absolute; + width: 100%; + box-sizing: border-box; + float: left; + border-radius: 1px !important; + border-top-left-radius: 0px !important; + border-top-right-radius: 0px !important; + overflow-x: hidden; + min-height: 30px; + max-height: 120px !important; + + .dark & { + background: @dark-bc-codehint-desc; + } +} + +.codehint-desc-type-details { + padding: 5px 15px 5px 15px; + color: @bc-codehint-desc-type-details; + font-weight: bold; + font-size: 1.2em; + line-height: inherit; + + .dark & { + color: @dark-bc-codehint-desc-type-details; + } +} + +.codehint-desc-documentation { + padding: 5px 15px 5px 15px; + color: @bc-codehint-desc-documentation; + font-size: 1.1em; + white-space: pre-wrap; + + .dark & { + color: @dark-bc-codehint-desc-documentation; + } +} + #context-menu-bar { margin: 0; } diff --git a/src/utils/Global.js b/src/utils/Global.js index 76c85cfdf1d..69a9189799c 100644 --- a/src/utils/Global.js +++ b/src/utils/Global.js @@ -83,6 +83,24 @@ define(function (require, exports, module) { global.brackets.platform = "win"; } + // Expose platform info for build applicability consumption + global.brackets.getPlatformInfo = function () { + var OS = ""; + + if (/Windows|Win32|WOW64|Win64/.test(window.navigator.userAgent)) { + OS = "WIN"; + } else if (/Mac/.test(window.navigator.userAgent)) { + OS = "OSX"; + } else if (/Linux|X11/.test(window.navigator.userAgent)) { + OS = "LINUX32"; + if (/x86_64/.test(window.navigator.appVersion + window.navigator.userAgent)) { + OS = "LINUX64"; + } + } + + return OS; + }; + global.brackets.inBrowser = !global.brackets.hasOwnProperty("fs"); // Are we in a desktop shell with a native menu bar? diff --git a/src/utils/HealthLogger.js b/src/utils/HealthLogger.js index 6125314d616..48d07ef0bce 100644 --- a/src/utils/HealthLogger.js +++ b/src/utils/HealthLogger.js @@ -24,6 +24,7 @@ /** * Utilities functions related to Health Data logging */ +/*global Map*/ define(function (require, exports, module) { "use strict"; @@ -36,7 +37,20 @@ define(function (require, exports, module) { EventDispatcher = require("utils/EventDispatcher"), HEALTH_DATA_STATE_KEY = "HealthData.Logs", - logHealthData = true; + logHealthData = true, + analyticsEventMap = new Map(); + + var commonStrings = { USAGE: "usage", + FILE_OPEN: "fileOpen", + FILE_NEW: "newfile", + FILE_SAVE: "fileSave", + FILE_CLOSE: "fileClose", + LANGUAGE_CHANGE: "languageChange", + LANGUAGE_SERVER_PROTOCOL: "languageServerProtocol", + CODE_HINTS: "codeHints", + PARAM_HINTS: "parameterHints", + JUMP_TO_DEF: "jumpToDefinition" + }; EventDispatcher.makeEventDispatcher(exports); @@ -165,6 +179,92 @@ define(function (require, exports, module) { fileEncCountMap[encoding]++; setHealthData(healthData); } + + + sendAnalyticsData(commonStrings.USAGE + commonStrings.FILE_OPEN + language._name, + commonStrings.USAGE, + commonStrings.FILE_OPEN, + language._name.toLowerCase() + ); + + } + + /** + * Whenever a file is saved call this function. + * The function will send the analytics Data + * We only log the standard filetypes and fileSize + * @param {String} filePath The path of the file to be registered + */ + function fileSaved(docToSave) { + if (!docToSave) { + return; + } + var fileType = docToSave.language ? docToSave.language._name : ""; + sendAnalyticsData(commonStrings.USAGE + commonStrings.FILE_SAVE + fileType, + commonStrings.USAGE, + commonStrings.FILE_SAVE, + fileType.toLowerCase() + ); + } + + /** + * Whenever a file is closed call this function. + * The function will send the analytics Data. + * We only log the standard filetypes and fileSize + * @param {String} filePath The path of the file to be registered + */ + function fileClosed(file) { + if (!file) { + return; + } + var language = LanguageManager.getLanguageForPath(file._path), + size = -1; + + function _sendData(fileSize) { + var subType = ""; + + if(fileSize/1024 <= 1) { + + if(fileSize < 0) { + subType = ""; + } + if(fileSize <= 10) { + subType = "Size_0_10KB"; + } else if (fileSize <= 50) { + subType = "Size_10_50KB"; + } else if (fileSize <= 100) { + subType = "Size_50_100KB"; + } else if (fileSize <= 500) { + subType = "Size_100_500KB"; + } else { + subType = "Size_500KB_1MB"; + } + + } else { + fileSize = fileSize/1024; + if(fileSize <= 2) { + subType = "Size_1_2MB"; + } else if(fileSize <= 5) { + subType = "Size_2_5MB"; + } else { + subType = "Size_Above_5MB"; + } + } + + sendAnalyticsData(commonStrings.USAGE + commonStrings.FILE_CLOSE + language._name + subType, + commonStrings.USAGE, + commonStrings.FILE_CLOSE, + language._name.toLowerCase(), + subType + ); + } + + file.stat(function(err, fileStat) { + if(!err) { + size = fileStat.size.valueOf()/1024; + } + _sendData(size); + }); } /** @@ -222,9 +322,10 @@ define(function (require, exports, module) { * needs to be logged- should be a js var compatible string */ function sendAnalyticsData(eventName, eventCategory, eventSubCategory, eventType, eventSubType) { - var isEventDataAlreadySent = PreferencesManager.getViewState(eventName), + var isEventDataAlreadySent = analyticsEventMap.get(eventName), isHDTracking = PreferencesManager.getExtensionPrefs("healthData").get("healthDataTracking"), eventParams = {}; + if (isHDTracking && !isEventDataAlreadySent && eventName && eventCategory) { eventParams = { eventName: eventName, @@ -243,11 +344,14 @@ define(function (require, exports, module) { exports.getAggregatedHealthData = getAggregatedHealthData; exports.clearHealthData = clearHealthData; exports.fileOpened = fileOpened; + exports.fileSaved = fileSaved; + exports.fileClosed = fileClosed; exports.setProjectDetail = setProjectDetail; exports.searchDone = searchDone; exports.setHealthLogsEnabled = setHealthLogsEnabled; exports.shouldLogHealthData = shouldLogHealthData; exports.init = init; + exports.sendAnalyticsData = sendAnalyticsData; // constants // searchType for searchDone() @@ -262,5 +366,6 @@ define(function (require, exports, module) { exports.SEARCH_CASE_SENSITIVE = "searchCaseSensitive"; // A new search context on search bar up-Gives an idea of number of times user did a discrete search exports.SEARCH_NEW = "searchNew"; - exports.sendAnalyticsData = sendAnalyticsData; + exports.commonStrings = commonStrings; + exports.analyticsEventMap = analyticsEventMap; }); diff --git a/src/utils/UpdateNotification.js b/src/utils/UpdateNotification.js index 8bef34b0ac7..ed0ec018a00 100644 --- a/src/utils/UpdateNotification.js +++ b/src/utils/UpdateNotification.js @@ -261,6 +261,13 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Checks whether a build is applicable to the current platform. + */ + function _checkBuildApplicability(buildInfo) { + return !buildInfo.platforms || buildInfo.platforms[brackets.getPlatformInfo()]; + } + /** * Return a new array of version information that is newer than "buildNumber". * Returns null if there is no new version information. @@ -270,20 +277,23 @@ define(function (require, exports, module) { // should get through the search quickly. var lastIndex = 0; var len = versionInfo.length; + var versionEntry; + var validBuildEntries; while (lastIndex < len) { - if (versionInfo[lastIndex].buildNumber <= buildNumber) { + versionEntry = versionInfo[lastIndex]; + if (versionEntry.buildNumber <= buildNumber) { break; } lastIndex++; } if (lastIndex > 0) { - return versionInfo.slice(0, lastIndex); + // Filter recent update entries based on applicability to current platform + validBuildEntries = versionInfo.slice(0, lastIndex).filter(_checkBuildApplicability); } - // No new version info - return null; + return validBuildEntries; } /** @@ -446,7 +456,7 @@ define(function (require, exports, module) { return; } - if (allUpdates) { + if (allUpdates && allUpdates.length > 0) { // Always show the "update available" icon if any updates are available var $updateNotification = $("#update-notification"); @@ -512,7 +522,12 @@ define(function (require, exports, module) { */ function handleUpdateProcess(updates) { var handler = _updateProcessHandler || _defaultUpdateProcessHandler; - handler(updates); + var initSuccess = handler(updates); + if (_updateProcessHandler && !initSuccess) { + // Give a chance to default handler in case + // the auot update mechanism has failed. + _defaultUpdateProcessHandler(updates); + } } /** diff --git a/src/widgets/InlineMenu.js b/src/widgets/InlineMenu.js index 685b4bb3d13..30b34c6a2e5 100644 --- a/src/widgets/InlineMenu.js +++ b/src/widgets/InlineMenu.js @@ -413,6 +413,19 @@ define(function (require, exports, module) { } }; + /** + * Displays the last menu which was closed due to Scrolling + */ + InlineMenu.prototype.openRemovedMenu = function () { + if (this.opened === true) { + if (this.$menu && !this.$menu.hasClass("open")) { + var menuPos = this._calcMenuLocation(); + this.$menu.addClass("open") + .css({"left": menuPos.left, "top": menuPos.top, "width": menuPos.width + "px"}); + } + } + }; + /** * Closes the menu */ diff --git a/tasks/npm-install.js b/tasks/npm-install.js index cad79951927..76434dce5e1 100644 --- a/tasks/npm-install.js +++ b/tasks/npm-install.js @@ -42,9 +42,10 @@ module.exports = function (grunt) { temp.track(); - function runNpmInstall(where, callback) { - grunt.log.writeln("running npm install --production in " + where); - exec('npm install --production', { cwd: './' + where }, function (err, stdout, stderr) { + function runNpmInstall(where, callback, includeDevDependencies) { + var envFlag = includeDevDependencies ? "" : " --production"; + grunt.log.writeln("running npm install" + envFlag + " in " + where); + exec('npm install' + envFlag, { cwd: './' + where }, function (err, stdout, stderr) { if (err) { grunt.log.error(stderr); } else { @@ -71,7 +72,7 @@ module.exports = function (grunt) { grunt.registerTask("npm-install-src", "Install node_modules to the src folder", function () { var _done = this.async(), - dirs = ["src", "src/JSUtils", "src/JSUtils/node"], + dirs = ["src", "src/JSUtils", "src/JSUtils/node", "src/languageTools/LanguageClient"], done = _.after(dirs.length, _done); dirs.forEach(function (dir) { runNpmInstall(dir, function (err) { @@ -99,10 +100,34 @@ module.exports = function (grunt) { }); }); + grunt.registerTask("npm-install-test", "Install node_modules for tests", function () { + var _done = this.async(); + var testDirs = [ + "spec/LanguageTools-test-files" + ]; + testDirs.forEach(function (dir) { + glob("test/" + dir + "/**/package.json", function (err, files) { + if (err) { + grunt.log.error(err); + return _done(false); + } + files = files.filter(function (path) { + return path.indexOf("node_modules") === -1; + }); + var done = _.after(files.length, _done); + files.forEach(function (file) { + runNpmInstall(path.dirname(file), function (err) { + return err ? _done(false) : done(); + }, true); + }); + }); + }); + }); + grunt.registerTask( "npm-install-source", "Install node_modules for src folder and default extensions which have package.json defined", - ["npm-install-src", "copy:thirdparty", "npm-install-extensions"] + ["npm-install-src", "copy:thirdparty", "npm-install-extensions", "npm-install-test"] ); function getNodeModulePackageUrl(extensionName) { diff --git a/test/SpecRunner.js b/test/SpecRunner.js index 6b25937ed24..075c7c48013 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -102,6 +102,18 @@ define(function (require, exports, module) { require("thirdparty/CodeMirror/addon/mode/overlay"); require("thirdparty/CodeMirror/addon/search/searchcursor"); require("thirdparty/CodeMirror/keymap/sublime"); + + //load Language Tools Module + require("languageTools/PathConverters"); + require("languageTools/LanguageTools"); + require("languageTools/ClientLoader"); + require("languageTools/BracketsToNodeInterface"); + require("languageTools/DefaultProviders"); + require("languageTools/DefaultEventHandlers"); + + //load language features + require("features/ParameterHintsManager"); + require("features/JumpToDefManager"); var selectedSuites, params = new UrlParams(), diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index b32c99f5977..5c9b54d5101 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -61,6 +61,7 @@ define(function (require, exports, module) { require("spec/JSONUtils-test"); require("spec/KeyBindingManager-test"); require("spec/LanguageManager-test"); + require("spec/LanguageTools-test"); require("spec/LiveDevelopment-test"); require("spec/LiveDevelopmentMultiBrowser-test"); require("spec/LowLevelFileIO-test"); diff --git a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js new file mode 100644 index 00000000000..0f42ff886bc --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js @@ -0,0 +1,121 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + path = require("path"), + clientName = "CommunicationTestClient", + client = null, + modulePath = null, + getPort = require("get-port"), + relativeLSPathArray = ["..", "..", "server", "lsp-test-server", "main.js"], + FORWARD_SLASH = "/", + BACKWARD_SLASH = "\\", + defaultPort = 3000; + +function getServerOptionsForSocket() { + return new Promise(function (resolve, reject) { + var serverPath = modulePath.split(BACKWARD_SLASH) + .join(FORWARD_SLASH).split(FORWARD_SLASH).concat(relativeLSPathArray) + .join(FORWARD_SLASH); + + getPort({ + port: defaultPort + }) + .then(function (port) { + + var serverOptions = { + module: serverPath, + communication: { + type: "socket", + port: port + } + }; + resolve(serverOptions); + }) + .catch(reject); + + }); +} + +function getServerOptions(type) { + var serverPath = modulePath.split(BACKWARD_SLASH) + .join(FORWARD_SLASH).split(FORWARD_SLASH).concat(relativeLSPathArray) + .join(FORWARD_SLASH); + + serverPath = path.resolve(serverPath); + + var serverOptions = { + module: serverPath, + communication: type + }; + + return serverOptions; +} + +function setModulePath(params) { + modulePath = params.modulePath.slice(0, params.modulePath.length - 1); + + return Promise.resolve(); +} + +function setOptions(params) { + if (!params || !params.communicationType) { + return Promise.reject("Can't start server because no communication type provided"); + } + + var cType = params.communicationType, + options = { + serverOptions: getServerOptions(cType) + }; + + client.setOptions(options); + + return Promise.resolve("Server options set successfully"); +} + +function setOptionsForSocket() { + return new Promise(function (resolve, reject) { + getServerOptionsForSocket() + .then(function (serverOptions) { + var options = { + serverOptions: serverOptions + }; + client.setOptions(options); + + resolve(); + }).catch(reject); + }); +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnRequestHandler('setModulePath', setModulePath); + client.addOnRequestHandler('setOptions', setOptions); + client.addOnRequestHandler('setOptionsForSocket', setOptionsForSocket); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/main.js b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/main.js new file mode 100644 index 00000000000..52c8ce0d28c --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/main.js @@ -0,0 +1,68 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "CommunicationTestClient", + clientPromise = null, + client = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(function (textClient) { + client = textClient; + + client.sendCustomRequest({ + messageType: "brackets", + type: "setModulePath", + params: { + modulePath: ExtensionUtils.getModulePath(module) + } + }).then(retval.resolve); + + + }, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; + + exports.getClient = function () { + return client; + }; +}); diff --git a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/package.json b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/package.json new file mode 100644 index 00000000000..cc25d2ac62d --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "get-port": "^4.2.0" + } +} diff --git a/test/spec/LanguageTools-test-files/clients/FeatureClient/client.js b/test/spec/LanguageTools-test-files/clients/FeatureClient/client.js new file mode 100644 index 00000000000..95050e7e0b8 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/FeatureClient/client.js @@ -0,0 +1,73 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + path = require("path"), + clientName = "FeatureClient", + client = null, + modulePath = null, + relativeLSPathArray = ["..", "..", "server", "lsp-test-server", "main.js"], + FORWARD_SLASH = "/", + BACKWARD_SLASH = "\\"; + +function getServerOptions() { + var serverPath = modulePath.split(BACKWARD_SLASH) + .join(FORWARD_SLASH).split(FORWARD_SLASH).concat(relativeLSPathArray) + .join(FORWARD_SLASH); + + serverPath = path.resolve(serverPath); + + var serverOptions = { + module: serverPath //node should fork this + }; + + return serverOptions; +} + +function setModulePath(params) { + modulePath = params.modulePath.slice(0, params.modulePath.length - 1); + + return Promise.resolve(); +} + +function setOptions(params) { + var options = { + serverOptions: getServerOptions() + }; + + client.setOptions(options); + + return Promise.resolve("Server options set successfully"); +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnRequestHandler('setModulePath', setModulePath); + client.addOnRequestHandler('setOptions', setOptions); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/FeatureClient/main.js b/test/spec/LanguageTools-test-files/clients/FeatureClient/main.js new file mode 100644 index 00000000000..8243d14fd5f --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/FeatureClient/main.js @@ -0,0 +1,74 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "FeatureClient", + clientPromise = null, + client = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(function (textClient) { + client = textClient; + + client.sendCustomRequest({ + messageType: "brackets", + type: "setModulePath", + params: { + modulePath: ExtensionUtils.getModulePath(module) + } + }).then(function () { + return client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions" + }); + }).then(function () { + retval.resolve(); + }); + + }, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; + + exports.getClient = function () { + return client; + }; +}); diff --git a/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/client.js b/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/client.js new file mode 100644 index 00000000000..15ae4c8de3d --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/client.js @@ -0,0 +1,130 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + clientName = "InterfaceTestClient", + client = null; + +function notificationMethod(params) { + switch (params.action) { + case 'acknowledgement': + { + client._notifyBrackets({ + type: "acknowledge", + params: { + acknowledgement: true, + clientName: clientName + } + }); + break; + } + case 'nodeSyncRequest': + { + var syncRequest = client._requestBrackets({ + type: "nodeSyncRequest", + params: { + syncRequest: true, + clientName: clientName + } + }); + + syncRequest.then(function (value) { + client._notifyBrackets({ + type: "validateSyncRequest", + params: { + syncRequestResult: value, + clientName: clientName + } + }); + }); + break; + } + case 'nodeAsyncRequestWhichResolves': + { + var asyncRequestS = client._requestBrackets({ + type: "nodeAsyncRequestWhichResolves", + params: { + asyncRequest: true, + clientName: clientName + } + }); + + asyncRequestS.then(function (value) { + client._notifyBrackets({ + type: "validateAsyncSuccess", + params: { + asyncRequestResult: value, + clientName: clientName + } + }); + }); + break; + } + case 'nodeAsyncRequestWhichFails': + { + var asyncRequestE = client._requestBrackets({ + type: "nodeAsyncRequestWhichFails", + params: { + asyncRequest: true, + clientName: clientName + } + }); + + asyncRequestE.catch(function (value) { + client._notifyBrackets({ + type: "validateAsyncFail", + params: { + asyncRequestError: value, + clientName: clientName + } + }); + }); + break; + } + } +} + +function requestMethod(params) { + switch (params.action) { + case 'resolve': + { + return Promise.resolve("resolved"); + } + case 'reject': + { + return Promise.reject("rejected"); + } + } +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnNotificationHandler("notificationMethod", notificationMethod); + client.addOnRequestHandler('requestMethod', requestMethod); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/main.js b/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/main.js new file mode 100644 index 00000000000..5889e5a3b37 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/main.js @@ -0,0 +1,59 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "InterfaceTestClient", + clientPromise = null, + client = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(function (textClient) { + client = textClient; + retval.resolve(); + }, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; + + exports.getClient = function () { + return client; + }; +}); diff --git a/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/client.js b/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/client.js new file mode 100644 index 00000000000..9dd77848740 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/client.js @@ -0,0 +1,37 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ + +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + clientName = "LoadSimpleClient", + client = null; + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/main.js b/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/main.js new file mode 100644 index 00000000000..c4160a98cfe --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/main.js @@ -0,0 +1,51 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "LoadSimpleClient", + clientPromise = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(retval.resolve, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; +}); diff --git a/test/spec/LanguageTools-test-files/clients/ModuleTestClient/client.js b/test/spec/LanguageTools-test-files/clients/ModuleTestClient/client.js new file mode 100644 index 00000000000..2f5c22c2f64 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/ModuleTestClient/client.js @@ -0,0 +1,73 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + path = require("path"), + clientName = "ModuleTestClient", + client = null, + modulePath = null, + relativeLSPathArray = ["..", "..", "server", "lsp-test-server", "main.js"], + FORWARD_SLASH = "/", + BACKWARD_SLASH = "\\"; + +function getServerOptions() { + var serverPath = modulePath.split(BACKWARD_SLASH) + .join(FORWARD_SLASH).split(FORWARD_SLASH).concat(relativeLSPathArray) + .join(FORWARD_SLASH); + + serverPath = path.resolve(serverPath); + + var serverOptions = { + module: serverPath //node should fork this + }; + + return serverOptions; +} + +function setModulePath(params) { + modulePath = params.modulePath.slice(0, params.modulePath.length - 1); + + return Promise.resolve(); +} + +function setOptions(params) { + var options = { + serverOptions: getServerOptions() + }; + + client.setOptions(options); + + return Promise.resolve("Server options set successfully"); +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnRequestHandler('setModulePath', setModulePath); + client.addOnRequestHandler('setOptions', setOptions); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/ModuleTestClient/main.js b/test/spec/LanguageTools-test-files/clients/ModuleTestClient/main.js new file mode 100644 index 00000000000..b3507f9d887 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/ModuleTestClient/main.js @@ -0,0 +1,74 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "ModuleTestClient", + clientPromise = null, + client = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(function (textClient) { + client = textClient; + + client.sendCustomRequest({ + messageType: "brackets", + type: "setModulePath", + params: { + modulePath: ExtensionUtils.getModulePath(module) + } + }).then(function () { + return client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions" + }); + }).then(function () { + retval.resolve(); + }); + + }, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; + + exports.getClient = function () { + return client; + }; +}); diff --git a/test/spec/LanguageTools-test-files/clients/OptionsTestClient/client.js b/test/spec/LanguageTools-test-files/clients/OptionsTestClient/client.js new file mode 100644 index 00000000000..d038105cae0 --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/OptionsTestClient/client.js @@ -0,0 +1,145 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ + +"use strict"; + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + path = require("path"), + cp = require("child_process"), + clientName = "OptionsTestClient", + client = null, + modulePath = null, + relativeLSPathArray = ["..", "..", "server", "lsp-test-server"], + FORWARD_SLASH = "/", + BACKWARD_SLASH = "\\"; + +function getServerOptions(type) { + var serverPath = modulePath.split(BACKWARD_SLASH) + .join(FORWARD_SLASH).split(FORWARD_SLASH).concat(relativeLSPathArray) + .join(FORWARD_SLASH); + + var newEnv = process.env; + newEnv.CUSTOMENVVARIABLE = "ANYTHING"; + + serverPath = path.resolve(serverPath); + var serverOptions = null; + + switch (type) { + case 'runtime': + { + // [runtime] [execArgs] [module] [args (with communication args)] (with options[env, cwd]) + serverOptions = { + runtime: process.execPath, //Path to node but could be anything, like php or perl + module: "main.js", + args: [ + "--server-args" //module args + ], //Arguments to process + options: { + cwd: serverPath, //The current directory where main.js is located + env: newEnv, //The process will be started CUSTOMENVVARIABLE in its environment + execArgv: [ + "--no-warnings", + "--no-deprecation" //runtime executable args + ] + }, + communication: "ipc" + }; + break; + } + case 'function': + { + serverOptions = function () { + return new Promise(function (resolve, reject) { + var serverProcess = cp.spawn(process.execPath, [ + "main.js", + "--stdio" //Have to add communication args manually + ], { + cwd: serverPath + }); + + if (serverProcess && serverProcess.pid) { + resolve({ + process: serverProcess + }); + } else { + reject("Couldn't create server process"); + } + }); + }; + break; + } + case 'command': + { + // [command] [args] (with options[env, cwd]) + serverOptions = { + command: process.execPath, //Path to executable, mostly runtime + args: [ + "--no-warnings", + "--no-deprecation", + "main.js", + "--stdio", //Have to add communication args manually + "--server-args" + ], //Arguments to process, ORDER WILL MATTER + options: { + cwd: serverPath, + env: newEnv //The process will be started CUSTOMENVVARIABLE in its environment + } + }; + break; + } + } + + return serverOptions; +} + +function setModulePath(params) { + modulePath = params.modulePath.slice(0, params.modulePath.length - 1); + + return Promise.resolve(); +} + +function setOptions(params) { + if (!params || !params.optionsType) { + return Promise.reject("Can't start server because no options type provided"); + } + + var oType = params.optionsType, + options = { + serverOptions: getServerOptions(oType) + }; + + client.setOptions(options); + + return Promise.resolve("Server options set successfully"); +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnRequestHandler('setModulePath', setModulePath); + client.addOnRequestHandler('setOptions', setOptions); +} + +exports.init = init; diff --git a/test/spec/LanguageTools-test-files/clients/OptionsTestClient/main.js b/test/spec/LanguageTools-test-files/clients/OptionsTestClient/main.js new file mode 100644 index 00000000000..67610b9f0ed --- /dev/null +++ b/test/spec/LanguageTools-test-files/clients/OptionsTestClient/main.js @@ -0,0 +1,68 @@ +/* + * 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 }]*/ +/*eslint indent: 0*/ +define(function (require, exports, module) { + "use strict"; + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "OptionsTestClient", + clientPromise = null, + client = null; + + AppInit.appReady(function () { + clientPromise = LanguageTools.initiateToolingService(clientName, clientFilePath, ['unknown']); + }); + + exports.initExtension = function () { + var retval = $.Deferred(); + + if ($.isFunction(clientPromise.promise)) { + clientPromise.then(function (textClient) { + client = textClient; + + client.sendCustomRequest({ + messageType: "brackets", + type: "setModulePath", + params: { + modulePath: ExtensionUtils.getModulePath(module) + } + }).then(retval.resolve); + + + }, retval.reject); + } else { + retval.reject(); + } + + return retval; + }; + + exports.getClient = function () { + return client; + }; +}); diff --git a/test/spec/LanguageTools-test-files/project/sample1.txt b/test/spec/LanguageTools-test-files/project/sample1.txt new file mode 100644 index 00000000000..8de75dcb4d7 --- /dev/null +++ b/test/spec/LanguageTools-test-files/project/sample1.txt @@ -0,0 +1 @@ +This has some text. \ No newline at end of file diff --git a/test/spec/LanguageTools-test-files/project/sample2.txt b/test/spec/LanguageTools-test-files/project/sample2.txt new file mode 100644 index 00000000000..9289cdf2601 --- /dev/null +++ b/test/spec/LanguageTools-test-files/project/sample2.txt @@ -0,0 +1 @@ +This has error text. \ No newline at end of file diff --git a/test/spec/LanguageTools-test-files/server/lsp-test-server/main.js b/test/spec/LanguageTools-test-files/server/lsp-test-server/main.js new file mode 100644 index 00000000000..2e0358eab1d --- /dev/null +++ b/test/spec/LanguageTools-test-files/server/lsp-test-server/main.js @@ -0,0 +1,255 @@ +/* + * 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-env es6, node*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +/*eslint indent: 0*/ +'use strict'; + +var vls = require("vscode-languageserver"), + connection = vls.createConnection(vls.ProposedFeatures.all); + +connection.onInitialize(function (params) { + return { + capabilities: { + textDocumentSync: 1, + completionProvider: { + resolveProvider: true, + triggerCharacters: [ + '=', + ' ', + '$', + '-', + '&' + ] + }, + definitionProvider: true, + signatureHelpProvider: { + triggerCharacters: [ + '-', + '[', + ',', + ' ', + '=' + ] + }, + "workspaceSymbolProvider": "true", + "documentSymbolProvider": "true", + "referencesProvider": "true" + } + }; +}); + +connection.onInitialized(function () { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.InitializedNotification.type._method + } + }); + + connection.workspace.onDidChangeWorkspaceFolders(function (params) { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.DidChangeWorkspaceFoldersNotification.type._method, + params: params + } + }); + }); +}); + +connection.onCompletion(function (params) { + return { + received: { + type: vls.CompletionRequest.type._method, + params: params + } + }; +}); + +connection.onSignatureHelp(function (params) { + return { + received: { + type: vls.SignatureHelpRequest.type._method, + params: params + } + }; +}); + +connection.onCompletionResolve(function (params) { + return { + received: { + type: vls.CompletionResolveRequest.type._method, + params: params + } + }; +}); + +connection.onDefinition(function (params) { + return { + received: { + type: vls.DefinitionRequest.type._method, + params: params + } + }; +}); + +connection.onDeclaration(function (params) { + return { + received: { + type: vls.DeclarationRequest.type._method, + params: params + } + }; +}); + +connection.onImplementation(function (params) { + return { + received: { + type: vls.ImplementationRequest.type._method, + params: params + } + }; +}); + +connection.onDocumentSymbol(function (params) { + return { + received: { + type: vls.DocumentSymbolRequest.type._method, + params: params + } + }; +}); + +connection.onWorkspaceSymbol(function (params) { + return { + received: { + type: vls.WorkspaceSymbolRequest.type._method, + params: params + } + }; +}); + +connection.onDidOpenTextDocument(function (params) { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.DidOpenTextDocumentNotification.type._method, + params: params + } + }); +}); + +connection.onDidChangeTextDocument(function (params) { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.DidChangeTextDocumentNotification.type._method, + params: params + } + }); +}); + +connection.onDidCloseTextDocument(function (params) { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.DidCloseTextDocumentNotification.type._method, + params: params + } + }); +}); + +connection.onDidSaveTextDocument(function (params) { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: vls.DidSaveTextDocumentNotification.type._method, + params: params + } + }); +}); + +connection.onNotification(function (type, params) { + switch (type) { + case "custom/triggerDiagnostics": + { + connection.sendDiagnostics({ + received: { + type: type, + params: params + } + }); + break; + } + case "custom/getNotification": + { + connection.sendNotification("custom/serverNotification", { + received: { + type: type, + params: params + } + }); + break; + } + case "custom/getRequest": + { + connection.sendRequest("custom/serverRequest", { + received: { + type: type, + params: params + } + }).then(function (resolveResponse) { + connection.sendNotification("custom/requestSuccessNotification", { + received: { + type: "custom/requestSuccessNotification", + params: resolveResponse + } + }); + }).catch(function (rejectResponse) { + connection.sendNotification("custom/requestFailedNotification", { + received: { + type: "custom/requestFailedNotification", + params: rejectResponse + } + }); + }); + break; + } + default: + { + connection.sendNotification(vls.LogMessageNotification.type, { + received: { + type: type, + params: params + } + }); + } + } +}); + +connection.onRequest(function (type, params) { + return { + received: { + type: type, + params: params + } + }; +}); + +// Listen on the connection +connection.listen(); diff --git a/test/spec/LanguageTools-test-files/server/lsp-test-server/package.json b/test/spec/LanguageTools-test-files/server/lsp-test-server/package.json new file mode 100644 index 00000000000..17b9af0423e --- /dev/null +++ b/test/spec/LanguageTools-test-files/server/lsp-test-server/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "vscode-languageserver": "^5.3.0-next.1" + } +} diff --git a/test/spec/LanguageTools-test.js b/test/spec/LanguageTools-test.js new file mode 100644 index 00000000000..0b80a3db32d --- /dev/null +++ b/test/spec/LanguageTools-test.js @@ -0,0 +1,1599 @@ +/* + * 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 */ +/*global describe, it, expect, spyOn, runs, waitsForDone, waitsForFail, afterEach */ +/*eslint indent: 0*/ +/*eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + 'use strict'; + + // Load dependent modules + var ExtensionLoader = require("utils/ExtensionLoader"), + SpecRunnerUtils = require("spec/SpecRunnerUtils"), + LanguageClientWrapper = require("languageTools/LanguageClientWrapper"), + LanguageTools = require("languageTools/LanguageTools"), + EventDispatcher = require("utils/EventDispatcher"), + ToolingInfo = JSON.parse(brackets.getModule("text!languageTools/ToolingInfo.json")); + + var testPath = SpecRunnerUtils.getTestPath("/spec/LanguageTools-test-files"), + serverResponse = { + capabilities: { + textDocumentSync: 1, + completionProvider: { + resolveProvider: true, + triggerCharacters: [ + '=', + ' ', + '$', + '-', + '&' + ] + }, + definitionProvider: true, + signatureHelpProvider: { + triggerCharacters: [ + '-', + '[', + ',', + ' ', + '=' + ] + }, + "workspaceSymbolProvider": "true", + "documentSymbolProvider": "true", + "referencesProvider": "true" + } + }; + + describe("LanguageTools", function () { + function loadClient(name) { + var config = { + baseUrl: testPath + "/clients/" + name + }; + + return ExtensionLoader.loadExtension(name, config, "main"); + } + + function getExtensionFromContext(name) { + var extensionContext = brackets.libRequire.s.contexts[name]; + + return extensionContext && extensionContext.defined && extensionContext.defined.main; + } + + it("should load a simple test client extension", function () { + var promise, + consoleErrors = []; + + runs(function () { + var originalConsoleErrorFn = console.error; + spyOn(console, "error").andCallFake(function () { + originalConsoleErrorFn.apply(console, arguments); + + if (typeof arguments[0] === "string" && + arguments[0].includes("Error loading domain \"LoadSimpleClient\"")) { + consoleErrors.push(Array.prototype.join.call(arguments)); + } + }); + + promise = loadClient("LoadSimpleClient"); + + waitsForDone(promise, "loadClient"); + }); + + runs(function () { + expect(consoleErrors).toEqual([]); + expect(promise.state()).toBe("resolved"); + }); + }); + + describe("Brackets & Node Communication", function () { + var intefacePromise, + extension, + client; + + it("should load the interface client extension", function () { + runs(function () { + intefacePromise = loadClient("InterfaceTestClient"); + intefacePromise.done(function () { + extension = getExtensionFromContext("InterfaceTestClient"); + client = extension.getClient(); + }); + + waitsForDone(intefacePromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("InterfaceTestClient"); + }); + }); + + it("should receive acknowledgement notification after sending notification to node", function () { + var notificationStatus = false; + + function notifyWithPromise() { + var retval = $.Deferred(); + + client._addOnNotificationHandler("acknowledge", function (params) { + if (params.clientName === "InterfaceTestClient" && params.acknowledgement) { + notificationStatus = true; + retval.resolve(); + } + }); + + client.sendCustomNotification({ + messageType: "brackets", + type: "notificationMethod", + params: { + action: "acknowledgement" + } + }); + + return retval; + } + + runs(function () { + var notificationPromise = notifyWithPromise(); + + waitsForDone(notificationPromise, "NotificationInterface"); + }); + + runs(function () { + expect(notificationStatus).toBe(true); + }); + }); + + it("should send request to node which should resolve", function () { + var result = null; + + function requestWithPromise() { + return client.sendCustomRequest({ + messageType: "brackets", + type: "requestMethod", + params: { + action: "resolve" + } + }); + } + + runs(function () { + var requestPromise = requestWithPromise(); + requestPromise.done(function (returnVal) { + result = returnVal; + }); + + waitsForDone(requestPromise, "RequestInterface"); + }); + + runs(function () { + expect(result).toBe("resolved"); + }); + }); + + it("should send request to node which should reject", function () { + var result = null; + + function requestWithPromise() { + return client.sendCustomRequest({ + messageType: "brackets", + type: "requestMethod", + params: { + action: "reject" + } + }); + } + + runs(function () { + var requestPromise = requestWithPromise(); + requestPromise.fail(function (returnVal) { + result = returnVal; + }); + + waitsForFail(requestPromise, "RequestInterface"); + }); + + runs(function () { + expect(result).toBe("rejected"); + }); + }); + + it("should handle sync request from node side", function () { + var requestResult = null; + + function nodeRequestWithPromise() { + var retval = $.Deferred(); + + client._addOnRequestHandler("nodeSyncRequest", function (params) { + if (params.clientName === "InterfaceTestClient" && params.syncRequest) { + //We return value directly since it is a sync request + return "success"; + } + }); + + //trigger request from node side + client._addOnNotificationHandler("validateSyncRequest", function (params) { + if (params.clientName === "InterfaceTestClient" && params.syncRequestResult) { + requestResult = params.syncRequestResult; + retval.resolve(); + } + }); + + client.sendCustomNotification({ + messageType: "brackets", + type: "notificationMethod", + params: { + action: "nodeSyncRequest" + } + }); + + return retval; + } + + runs(function () { + var nodeRequestPromise = nodeRequestWithPromise(); + + waitsForDone(nodeRequestPromise, "NodeRequestInterface"); + }); + + runs(function () { + expect(requestResult).toEqual("success"); + }); + }); + + it("should handle async request from node side which is resolved", function () { + var requestResult = null; + + function nodeRequestWithPromise() { + var retval = $.Deferred(); + + client._addOnRequestHandler("nodeAsyncRequestWhichResolves", function (params) { + if (params.clientName === "InterfaceTestClient" && params.asyncRequest) { + //We return promise which can be resolved in async + return $.Deferred().resolve("success"); + } + }); + + //trigger request from node side + client._addOnNotificationHandler("validateAsyncSuccess", function (params) { + if (params.clientName === "InterfaceTestClient" && params.asyncRequestResult) { + requestResult = params.asyncRequestResult; + retval.resolve(); + } + }); + + client.sendCustomNotification({ + messageType: "brackets", + type: "notificationMethod", + params: { + action: "nodeAsyncRequestWhichResolves" + } + }); + + return retval; + } + + runs(function () { + var nodeRequestPromise = nodeRequestWithPromise(); + + waitsForDone(nodeRequestPromise, "NodeRequestInterface"); + }); + + runs(function () { + expect(requestResult).toEqual("success"); + }); + }); + + it("should handle async request from node side which fails", function () { + var requestResult = null; + + function nodeRequestWithPromise() { + var retval = $.Deferred(); + + client._addOnRequestHandler("nodeAsyncRequestWhichFails", function (params) { + if (params.clientName === "InterfaceTestClient" && params.asyncRequest) { + //We return promise which can be resolved in async + return $.Deferred().reject("error"); + } + }); + + //trigger request from node side + client._addOnNotificationHandler("validateAsyncFail", function (params) { + if (params.clientName === "InterfaceTestClient" && params.asyncRequestError) { + requestResult = params.asyncRequestError; + retval.resolve(); + } + }); + + client.sendCustomNotification({ + messageType: "brackets", + type: "notificationMethod", + params: { + action: "nodeAsyncRequestWhichFails" + } + }); + + return retval; + } + + runs(function () { + var nodeRequestPromise = nodeRequestWithPromise(); + + waitsForDone(nodeRequestPromise, "NodeRequestInterface"); + }); + + runs(function () { + expect(requestResult).toEqual("error"); + }); + }); + }); + + describe("Client Start and Stop Tests", function () { + var projectPath = testPath + "/project", + optionsPromise, + extension, + client = null; + + it("should start a simple module based client", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("ModuleTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("ModuleTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("ModuleTestClient"); + + startPromise = client.start({ + rootPath: projectPath + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should stop a simple module based client", function () { + var restartPromise, + restartStatus = false; + + runs(function () { + if (client) { + restartPromise = client.stop().done(function () { + return client.start({ + rootPath: projectPath + }); + }); + restartPromise.done(function () { + restartStatus = true; + }); + } + + waitsForDone(restartPromise, "RestartClient"); + }); + + runs(function () { + expect(restartStatus).toBe(true); + }); + }); + + + it("should stop a simple module based client", function () { + var stopPromise, + stopStatus = false; + + runs(function () { + if (client) { + stopPromise = client.stop(); + stopPromise.done(function () { + stopStatus = true; + client = null; + }); + } + + waitsForDone(stopPromise, "StopClient"); + }); + + runs(function () { + expect(stopStatus).toBe(true); + }); + }); + }); + + describe("Language Server Spawn Schemes", function () { + var projectPath = testPath + "/project", + optionsPromise, + extension, + client = null; + + afterEach(function () { + var stopPromise, + stopStatus = false; + + runs(function () { + if (client) { + stopPromise = client.stop(); + stopPromise.done(function () { + stopStatus = true; + client = null; + }); + } else { + stopStatus = true; + } + + waitsForDone(stopPromise, "StopClient"); + }); + + runs(function () { + expect(stopStatus).toBe(true); + }); + }); + + it("should start a simple module based client with node-ipc", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("CommunicationTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("CommunicationTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("CommunicationTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + communicationType: "ipc" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple module based client with stdio", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("CommunicationTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("CommunicationTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("CommunicationTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + communicationType: "stdio" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple module based client with pipe", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("CommunicationTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("CommunicationTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("CommunicationTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + communicationType: "pipe" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple module based client with socket", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("CommunicationTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("CommunicationTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("CommunicationTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptionsForSocket" + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple runtime based client", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("OptionsTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("OptionsTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("OptionsTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + optionsType: "runtime" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple function based client", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("OptionsTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("OptionsTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("OptionsTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + optionsType: "function" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should start a simple command based client", function () { + var startResult = false, + startPromise; + + runs(function () { + optionsPromise = loadClient("OptionsTestClient"); + optionsPromise.done(function () { + extension = getExtensionFromContext("OptionsTestClient"); + client = extension.getClient(); + }); + + waitsForDone(optionsPromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("OptionsTestClient"); + + startPromise = client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions", + params: { + optionsType: "command" + } + }).then(function () { + return client.start({ + rootPath: projectPath + }); + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + }); + + describe("Parameter validation for client based communication", function () { + var requestValidator = LanguageClientWrapper.validateRequestParams, + notificationValidator = LanguageClientWrapper.validateNotificationParams; + + var paramTemplateA = { + rootPath: "somePath" + }; + + var paramTemplateB = { + filePath: "somePath", + cursorPos: { + line: 1, + ch: 1 + } + }; + + var paramTemplateC = { + filePath: "somePath" + }; + + var paramTemplateD = { + filePath: "something", + fileContent: "something", + languageId: "something" + }; + + var paramTemplateE = { + filePath: "something", + fileContent: "something" + }; + + var paramTemplateF = { + foldersAdded: ["added"], + foldersRemoved: ["removed"] + }; + + it("should validate the params for request: client.start", function () { + var params = Object.assign({}, paramTemplateA), + retval = requestValidator(ToolingInfo.LANGUAGE_SERVICE.START, params); + + var params2 = Object.assign({}, paramTemplateA); + params2["capabilities"] = { + feature: true + }; + var retval2 = requestValidator(ToolingInfo.LANGUAGE_SERVICE.START, params2); + + expect(retval).toEqual({ + rootPath: "somePath", + capabilities: false + }); + + expect(retval2).toEqual({ + rootPath: "somePath", + capabilities: { + feature: true + } + }); + }); + + it("should invalidate the params for request: client.start", function () { + var retval = requestValidator(ToolingInfo.LANGUAGE_SERVICE.START, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for request: client.{requestHints, requestParameterHints, gotoDefinition}", function () { + var params = Object.assign({}, paramTemplateB), + retval = requestValidator(ToolingInfo.FEATURES.CODE_HINTS, params); + + expect(retval).toEqual(paramTemplateB); + }); + + it("should invalidate the params for request: client.{requestHints, requestParameterHints, gotoDefinition}", function () { + var retval = requestValidator(ToolingInfo.FEATURES.CODE_HINTS, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for request: client.findReferences", function () { + var params = Object.assign({}, paramTemplateB), + retval = requestValidator(ToolingInfo.FEATURES.FIND_REFERENCES, params); + + var result = Object.assign({}, paramTemplateB); + result["includeDeclaration"] = false; + + expect(retval).toEqual(result); + }); + + it("should invalidate the params for request: client.findReferences", function () { + var retval = requestValidator(ToolingInfo.FEATURES.FIND_REFERENCES, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for request: client.requestSymbolsForDocument", function () { + var params = Object.assign({}, paramTemplateC), + retval = requestValidator(ToolingInfo.FEATURES.DOCUMENT_SYMBOLS, params); + + expect(retval).toEqual(paramTemplateC); + }); + + it("should invalidate the params for request: client.requestSymbolsForDocument", function () { + var retval = requestValidator(ToolingInfo.FEATURES.DOCUMENT_SYMBOLS, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for request: client.requestSymbolsForWorkspace", function () { + var params = Object.assign({}, { + query: 'a' + }), + retval = requestValidator(ToolingInfo.FEATURES.PROJECT_SYMBOLS, params); + + expect(retval).toEqual({ + query: 'a' + }); + }); + + it("should invalidate the params for request: client.requestSymbolsForWorkspace", function () { + var retval = requestValidator(ToolingInfo.FEATURES.PROJECT_SYMBOLS, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for notification: client.notifyTextDocumentOpened", function () { + var params = Object.assign({}, paramTemplateD), + retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED, params); + + expect(retval).toEqual(paramTemplateD); + }); + + it("should invalidate the params for notification: client.notifyTextDocumentOpened", function () { + var retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for notification: client.notifyTextDocumentChanged", function () { + var params = Object.assign({}, paramTemplateE), + retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED, params); + + expect(retval).toEqual(paramTemplateE); + }); + + it("should invalidate the params for notification: client.notifyTextDocumentChanged", function () { + var retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for notification: client.{notifyTextDocumentClosed, notifyTextDocumentSave}", function () { + var params = Object.assign({}, paramTemplateC), + retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED, params); + + expect(retval).toEqual(paramTemplateC); + }); + + it("should invalidate the params for notification: client.{notifyTextDocumentClosed, notifyTextDocumentSave}", function () { + var retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED, {}); + + expect(retval).toBeNull(); + }); + + it("should validate the params for notification: client.notifyProjectRootsChanged", function () { + var params = Object.assign({}, paramTemplateF), + retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED, params); + + expect(retval).toEqual(paramTemplateF); + }); + + it("should invalidate the params for notification: client.notifyProjectRootsChanged", function () { + var retval = notificationValidator(ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED, {}); + + expect(retval).toBeNull(); + }); + + it("should passthrough the params for request: client.sendCustomRequest", function () { + var params = Object.assign({}, { + a: 1, + b: 2 + }), + retval = requestValidator(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST, params); + + expect(retval).toEqual({ + a: 1, + b: 2 + }); + }); + + it("should passthrough the params for notification: client.sendCustomNotification", function () { + var params = Object.assign({}, { + a: 1, + b: 2 + }), + retval = notificationValidator(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION, params); + + expect(retval).toEqual({ + a: 1, + b: 2 + }); + }); + + it("should passthrough the params for any request if format is 'lsp'", function () { + var params = Object.assign({}, { + format: 'lsp', + a: 1, + b: 2 + }), + retval = requestValidator("AnyType", params); + + expect(retval).toEqual({ + format: 'lsp', + a: 1, + b: 2 + }); + }); + + it("should passthrough the params for any notification if format is 'lsp'", function () { + var params = Object.assign({}, { + format: 'lsp', + a: 1, + b: 2 + }), + retval = notificationValidator("AnyType", params); + + expect(retval).toEqual({ + format: 'lsp', + a: 1, + b: 2 + }); + }); + }); + + describe("Test LSP Request and Notifications", function () { + var projectPath = testPath + "/project", + featurePromise, + extension, + client = null, + docPath1 = projectPath + "/sample1.txt", + docPath2 = projectPath + "/sample2.txt", + pos = { + line: 1, + ch: 2 + }, + fileContent = "some content", + languageId = "unknown"; + + function createPromiseForNotification(type) { + var promise = $.Deferred(); + + switch (type) { + case "textDocument/publishDiagnostics": { + client.addOnCodeInspection(function (params) { + promise.resolve(params); + }); + break; + } + case "custom/serverNotification": + case "custom/requestSuccessNotification": + case "custom/requestFailedNotification": + { + client.onCustomNotification(type, function (params) { + promise.resolve(params); + }); + break; + } + default: { + client.addOnLogMessage(function (params) { + if (params.received && params.received.type && + params.received.type === type) { + promise.resolve(params); + } + }); + } + } + + return promise; + } + + it("should successfully start client", function () { + var startResult = false, + startPromise; + + runs(function () { + featurePromise = loadClient("FeatureClient"); + featurePromise.done(function () { + extension = getExtensionFromContext("FeatureClient"); + client = extension.getClient(); + }); + + waitsForDone(featurePromise); + }); + + runs(function () { + expect(client).toBeTruthy(); + expect(client._name).toEqual("FeatureClient"); + + client.onDynamicCapabilityRegistration(function () { + return $.Deferred().resolve(); + }); + + client.onDynamicCapabilityUnregistration(function () { + return $.Deferred().resolve(); + }); + + startPromise = client.start({ + rootPath: projectPath + }); + + startPromise.done(function (capabilities) { + startResult = capabilities; + }); + + waitsForDone(startPromise, "StartClient"); + }); + + runs(function () { + expect(startResult).toBeTruthy(); + expect(startResult).toEqual(serverResponse); + }); + }); + + it("should successfully requestHints with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.requestHints({ + filePath: docPath1, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully passthrough params with lsp format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.requestHints({ + format: 'lsp', + textDocument: { + uri: 'file:///somepath/project/sample1.txt' + }, + position: { + line: 1, + character: 2 + } + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse).toEqual({ + received: { + type: 'textDocument/completion', + params: { + textDocument: { + uri: 'file:///somepath/project/sample1.txt' + }, + position: { + line: 1, + character: 2 + } + } + } + }); + }); + }); + + it("should successfully getAdditionalInfoForHint", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.getAdditionalInfoForHint({ + hintItem: true + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse).toEqual({ + received: { + type: 'completionItem/resolve', + params: { + hintItem: true + } + } + }); + }); + }); + + it("should successfully requestParameterHints with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.requestParameterHints({ + filePath: docPath2, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully gotoDefinition with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.gotoDefinition({ + filePath: docPath2, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully gotoImplementation with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.gotoImplementation({ + filePath: docPath2, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully gotoDeclaration with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.gotoDeclaration({ + filePath: docPath2, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully findReferences with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.findReferences({ + filePath: docPath2, + cursorPos: pos + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully requestSymbolsForDocument with brackets format", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.requestSymbolsForDocument({ + filePath: docPath2 + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully requestSymbolsForWorkspace", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.requestSymbolsForWorkspace({ + query: "s" + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully sendCustomRequest to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = client.sendCustomRequest({ + type: "custom/serverRequest", + params: { + anyParam: true + } + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully notifyTextDocumentOpened to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("textDocument/didOpen"); + client.notifyTextDocumentOpened({ + languageId: languageId, + filePath: docPath1, + fileContent: fileContent + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully notifyTextDocumentClosed to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("textDocument/didClose"); + client.notifyTextDocumentClosed({ + filePath: docPath1 + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully notifyTextDocumentSave to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("textDocument/didSave"); + client.notifyTextDocumentSave({ + filePath: docPath2 + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully notifyTextDocumentChanged to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("textDocument/didChange"); + client.notifyTextDocumentChanged({ + filePath: docPath2, + fileContent: fileContent + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully notifyProjectRootsChanged to server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("workspace/didChangeWorkspaceFolders"); + client.notifyProjectRootsChanged({ + foldersAdded: ["path1", "path2"], + foldersRemoved: ["path3"] + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully get send custom notification to trigger diagnostics from server", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + requestPromise = createPromiseForNotification("textDocument/publishDiagnostics"); + client.sendCustomNotification({ + type: "custom/triggerDiagnostics" + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully create a custom event trigger for server notification", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + EventDispatcher.makeEventDispatcher(exports); + LanguageTools.listenToCustomEvent(exports, "triggerDiagnostics"); + client.addOnCustomEventHandler("triggerDiagnostics", function () { + client.sendCustomNotification({ + type: "custom/triggerDiagnostics" + }); + }); + requestPromise = createPromiseForNotification("textDocument/publishDiagnostics"); + exports.trigger("triggerDiagnostics"); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully handle a custom server notification", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + + requestPromise = createPromiseForNotification("custom/serverNotification"); + client.sendCustomNotification({ + type: "custom/getNotification" + }); + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerNotification"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully handle a custom server request on resolve", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + + requestPromise = createPromiseForNotification("custom/requestSuccessNotification"); + client.onCustomRequest("custom/serverRequest", function (params) { + return $.Deferred().resolve(params); + }); + + client.sendCustomNotification({ + type: "custom/getRequest" + }); + + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully handle a custom server request on reject", function () { + var requestPromise, + requestResponse = null; + + runs(function () { + + requestPromise = createPromiseForNotification("custom/requestFailedNotification"); + client.onCustomRequest("custom/serverRequest", function (params) { + return $.Deferred().reject(params); + }); + + client.sendCustomNotification({ + type: "custom/getRequest" + }); + + requestPromise.done(function (response) { + requestResponse = response; + }); + + waitsForDone(requestPromise, "ServerRequest"); + }); + + runs(function () { + expect(requestResponse.received).toBeTruthy(); + }); + }); + + it("should successfully stop client", function () { + var stopPromise, + stopStatus = false; + + runs(function () { + if (client) { + stopPromise = client.stop(); + stopPromise.done(function () { + stopStatus = true; + client = null; + }); + } + + waitsForDone(stopPromise, "StopClient"); + }); + + runs(function () { + expect(stopStatus).toBe(true); + }); + }); + }); + }); +});