From df7aea423a1e554f00d317078b5296870241cae2 Mon Sep 17 00:00:00 2001 From: Subhash Jha Date: Tue, 2 Apr 2019 15:05:43 +0530 Subject: [PATCH] Language Server Protocol Support for Brackets (#14606) * LSP Initial set of changes * Adding comments and a bit of cleanup * Adding php client for lsp * further cleanup * removing dependency on HintUtils * removing phpClient extension from this branch * Cleanup * fixing eslint errors * Refactoring code- Removing dependency on JSUtils ANd adding basic structure for client capabilities * Bug Fix: too many listeners were getting attached to node process + code cleanup * putting null check and settign capabilities to default values * reinitializing server on workspace change and moving out capabilities from client code * cleanup * First cut for LSP support in Brackets * First cut for LSP support in Brackets * Adding client infrastructure * Adding client infrastructure * Adding handlers on Language Client Proxy, fixing eslint errors * Adding handlers on Language Client Proxy, fixing eslint errors * Fixing protocol adapter * Fixing protocol adapter * Fix typo * Fix typo * Removing older implementation * Removing older implementation * Added error checks to the auto update mechanism. So in case the auto update mechansim fails, we will now give chance to the default update process Handler to handle the update mechanism (Which is essentially taking the user to brackets.io). (#14605) * First cut for LSP support in Brackets * First cut for LSP support in Brackets * Adding client infrastructure * Adding client infrastructure * Adding handlers on Language Client Proxy, fixing eslint errors * Adding handlers on Language Client Proxy, fixing eslint errors * Fixing protocol adapter * Fixing protocol adapter * Fix typo * Fix typo * Removing older implementation * Removing older implementation * Removing custom comments * Removing custom comments * Fixing Typo * Fixing Typo * Add missing Params in function call * Add missing Params in function call * Correcting message type, handlers * Correcting message type, handlers * Minor correction on active project change * Minor correction on active project change * Correcting the message format for didChange * Correcting the message format for didChange * Changing custom notification and request handlers, correcting typo, adding catch block for Connection * Changing custom notification and request handlers, correcting typo, adding catch block for Connection * Stop Creation of Multiple Language Servers * Stop Creation of Multiple Language Servers * Make Language Client Generic, address review comments * Make Language Client Generic, address review comments * Correcting param descriptions * Correcting param descriptions * Modifying events handling logic for Language Client, add formatting option for communication params * Modifying events handling logic for Language Client, add formatting option for communication params * Add handlers for node side * Add handlers for node side * Removing explicit param creation, substituting with appropriate checks * Removing explicit param creation, substituting with appropriate checks * Fixing lint errors in MessageHandler.js * Fixing lint errors in MessageHandler.js * Messaging related cleanup * Messaging related cleanup * Adding default providers and feature managers * Adding default providers and feature managers * Adding banner and fixing lint error * Adding banner and fixing lint error * fix spacing issue * fix spacing issue * Fix spacing issues * Fix spacing issues * Add filetype checks for all events, minor server info corrections * Add filetype checks for all events, minor server info corrections * Handling Reload with Extension Scenario, minor JumpToDef provider fix * Handling Reload with Extension Scenario, minor JumpToDef provider fix * Correcting Typo * Correcting Typo * Adding bug fixes * Adding bug fixes * Adding bug fixes 2 * Adding bug fixes 2 * Addressing Review: Fixing minor typo * Addressing Review: Fixing minor typo * Minor bug fixes, functionality enhancements * Minor bug fixes, functionality enhancements * Adding tests for Language Server Support: first cut * Adding tests for Language Server Support: first cut * Adding banner, fixing lint errors * Adding banner, fixing lint errors * Adding dependency related tasks * Adding dependency related tasks * Fixing npm environment string * Fixing npm environment string * Changing handler name * Changing handler name * Changing file name to ClientLoader * Changing file name to ClientLoader * Changing variable name appropriately * Changing variable name appropriately * Grunt related changes for build * Grunt related changes for build * Adding additional requests and notifications for handling various scenarios * Adding additional requests and notifications for handling various scenarios * Adding Path Converter Utilities * Adding Path Converter Utilities * Changing Ternary operator to OR operater * Changing Ternary operator to OR operater * Addressing review comments * Addressing review comments * Removing the handler for editor change, will be handled explicitely * Removing the handler for editor change, will be handled explicitely * Patching JavaScriptCodeHints * Patching JavaScriptCodeHints * Preferences infra for LanguageTools * Preferences infra for LanguageTools * Fixing JS ParameterHints * Fixing JS ParameterHints * Fixing Default Parameter Hints Provider * Fixing Default Parameter Hints Provider * Fixing Path Converters * Fixing Path Converters * Fixing Lint in PathConverters * Fixing Lint in PathConverters * Retaining Posix Path on Win * Retaining Posix Path on Win * Fixing lint errors * Fixing lint errors * Fixing Node side Utils * Fixing Node side Utils * Fixing Promise related Issues * Fixing Promise related Issues * Set Server Capability in Start call * Set Server Capability in Start call * Review Comments & Bug Fixes * Review Comments & Bug Fixes * Addressing Review Comments * Addressing Review Comments * Fixing Lint * Fixing Lint --- .eslintrc.js | 10 + Gruntfile.js | 3 + src/brackets.js | 13 + src/command/Commands.js | 2 +- src/editor/EditorManager.js | 76 - .../ParameterHintManager.js | 445 ----- .../ParameterHintTemplate.html | 4 - .../ParameterHintsProvider.js | 229 +++ .../default/JavaScriptCodeHints/keyboard.json | 11 - .../default/JavaScriptCodeHints/main.js | 69 +- .../default/JavaScriptCodeHints/unittests.js | 79 +- .../default/JavaScriptQuickEdit/unittests.js | 64 +- src/features/JumpToDefManager.js | 89 + src/features/ParameterHintsManager.js | 416 +++++ src/features/PriorityBasedRegistration.js | 138 ++ src/htmlContent/parameter-hint-template.html | 4 + src/languageTools/BracketsToNodeInterface.js | 121 ++ src/languageTools/ClientLoader.js | 128 ++ src/languageTools/DefaultEventHandlers.js | 193 ++ src/languageTools/DefaultProviders.js | 384 ++++ .../LanguageClient/Connection.js | 134 ++ .../LanguageClient/LanguageClient.js | 232 +++ .../LanguageClient/NodeToBracketsInterface.js | 213 +++ .../LanguageClient/ProtocolAdapter.js | 398 ++++ .../LanguageClient/ServerUtils.js | 427 +++++ src/languageTools/LanguageClient/Utils.js | 88 + src/languageTools/LanguageClient/package.json | 19 + src/languageTools/LanguageClientWrapper.js | 627 +++++++ src/languageTools/LanguageTools.js | 119 ++ src/languageTools/PathConverters.js | 82 + src/languageTools/ToolingInfo.json | 41 + .../node/RegisterLanguageClientInfo.js | 290 +++ .../styles/default_provider_style.css | 134 ++ src/nls/root/strings.js | 5 +- tasks/npm-install.js | 35 +- test/SpecRunner.js | 12 + test/UnitTestSuite.js | 1 + .../clients/CommunicationTestClient/client.js | 121 ++ .../clients/CommunicationTestClient/main.js | 68 + .../CommunicationTestClient/package.json | 5 + .../clients/FeatureClient/client.js | 73 + .../clients/FeatureClient/main.js | 74 + .../clients/InterfaceTestClient/client.js | 130 ++ .../clients/InterfaceTestClient/main.js | 59 + .../clients/LoadSimpleClient/client.js | 37 + .../clients/LoadSimpleClient/main.js | 51 + .../clients/ModuleTestClient/client.js | 73 + .../clients/ModuleTestClient/main.js | 74 + .../clients/OptionsTestClient/client.js | 145 ++ .../clients/OptionsTestClient/main.js | 68 + .../project/sample1.txt | 1 + .../project/sample2.txt | 1 + .../server/lsp-test-server/main.js | 255 +++ .../server/lsp-test-server/package.json | 5 + test/spec/LanguageTools-test.js | 1599 +++++++++++++++++ 55 files changed, 7527 insertions(+), 647 deletions(-) delete mode 100644 src/extensions/default/JavaScriptCodeHints/ParameterHintManager.js delete mode 100644 src/extensions/default/JavaScriptCodeHints/ParameterHintTemplate.html create mode 100644 src/extensions/default/JavaScriptCodeHints/ParameterHintsProvider.js delete mode 100644 src/extensions/default/JavaScriptCodeHints/keyboard.json create mode 100644 src/features/JumpToDefManager.js create mode 100644 src/features/ParameterHintsManager.js create mode 100644 src/features/PriorityBasedRegistration.js create mode 100644 src/htmlContent/parameter-hint-template.html create mode 100644 src/languageTools/BracketsToNodeInterface.js create mode 100644 src/languageTools/ClientLoader.js create mode 100644 src/languageTools/DefaultEventHandlers.js create mode 100644 src/languageTools/DefaultProviders.js create mode 100644 src/languageTools/LanguageClient/Connection.js create mode 100644 src/languageTools/LanguageClient/LanguageClient.js create mode 100644 src/languageTools/LanguageClient/NodeToBracketsInterface.js create mode 100644 src/languageTools/LanguageClient/ProtocolAdapter.js create mode 100644 src/languageTools/LanguageClient/ServerUtils.js create mode 100644 src/languageTools/LanguageClient/Utils.js create mode 100644 src/languageTools/LanguageClient/package.json create mode 100644 src/languageTools/LanguageClientWrapper.js create mode 100644 src/languageTools/LanguageTools.js create mode 100644 src/languageTools/PathConverters.js create mode 100644 src/languageTools/ToolingInfo.json create mode 100644 src/languageTools/node/RegisterLanguageClientInfo.js create mode 100644 src/languageTools/styles/default_provider_style.css create mode 100644 test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/CommunicationTestClient/main.js create mode 100644 test/spec/LanguageTools-test-files/clients/CommunicationTestClient/package.json create mode 100644 test/spec/LanguageTools-test-files/clients/FeatureClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/FeatureClient/main.js create mode 100644 test/spec/LanguageTools-test-files/clients/InterfaceTestClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/InterfaceTestClient/main.js create mode 100644 test/spec/LanguageTools-test-files/clients/LoadSimpleClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/LoadSimpleClient/main.js create mode 100644 test/spec/LanguageTools-test-files/clients/ModuleTestClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/ModuleTestClient/main.js create mode 100644 test/spec/LanguageTools-test-files/clients/OptionsTestClient/client.js create mode 100644 test/spec/LanguageTools-test-files/clients/OptionsTestClient/main.js create mode 100644 test/spec/LanguageTools-test-files/project/sample1.txt create mode 100644 test/spec/LanguageTools-test-files/project/sample2.txt create mode 100644 test/spec/LanguageTools-test-files/server/lsp-test-server/main.js create mode 100644 test/spec/LanguageTools-test-files/server/lsp-test-server/package.json create mode 100644 test/spec/LanguageTools-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 40331166ab1..4ff0306efcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -80,5 +80,15 @@ module.exports = { "Uint32Array": false, "WebSocket": false, "XMLHttpRequest": false + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "script", + "ecmaFeatures": { + "arrowFunctions": true, + "binaryLiterals": true, + "blockBindings": true, + "classes": true + } } }; diff --git a/Gruntfile.js b/Gruntfile.js index 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/src/brackets.js b/src/brackets.js index 9a21072cefd..4365249ef05 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"); @@ -155,6 +159,15 @@ define(function (require, exports, 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..dc147eeac6f 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -123,7 +123,7 @@ 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_LINE = "navigate.gotoLine"; // QuickOpen.js doGotoLine() exports.NAVIGATE_GOTO_FIRST_PROBLEM = "navigate.gotoFirstProblem"; // CodeInspection.js handleGotoFirstProblem() 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/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/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..c2fa615da6b --- /dev/null +++ b/src/languageTools/ClientLoader.js @@ -0,0 +1,128 @@ +/* + * 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"), + BracketsToNodeInterface = require("languageTools/BracketsToNodeInterface").BracketsToNodeInterface; + + //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 = new NodeDomain("LanguageClientInfo", _domainPath), + //Init node with Information required by Language Client + clientInfoLoadedPromise = clientInfoDomain.exec("initialize", _bracketsPath, ToolingInfo), + //Clients that have to be loaded once the LanguageClient info is successfully loaded on the + //node side. + pendingClientsToBeLoaded = []; + + //Attach success and failure function for the clientInfoLoadedPromise + clientInfoLoadedPromise.then(function () { + pendingClientsToBeLoaded.forEach(function (pendingClient) { + pendingClient.load(); + }); + }, function () { + console.log("Failed to Initialize LanguageClient Module Information."); + }); + + 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.state() === "pending") { + var pendingClient = { + load: _clientLoader.bind(null, clientName, clientFilePath, result) + }; + pendingClientsToBeLoaded.push(pendingClient); + } else { + _clientLoader(clientName, clientFilePath, result); + } + + return result; + } + + 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..d5cc5ad8a9b --- /dev/null +++ b/src/languageTools/DefaultProviders.js @@ -0,0 +1,384 @@ +/* + * 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 }]*/ +define(function (require, exports, module) { + "use strict"; + + var _ = brackets.getModule("thirdparty/lodash"); + + var EditorManager = require('editor/EditorManager'), + 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 CodeHintsProvider(client) { + this.client = client; + this.query = ""; + } + + 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, + start = { + line: cursor.line, + ch: cursor.ch - query.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.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.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(); + } + + LintingProvider.prototype.clearExistingResults = function (filePath) { + var filePathProvided = !!filePath; + + if (filePathProvided) { + this._results.delete(filePath); + } else { + //clear all results + this._results.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 + }); + }; + + LintingProvider.prototype.getInspectionResults = function (fileText, filePath) { + return this._results.get(filePath); + }; + + exports.CodeHintsProvider = CodeHintsProvider; + exports.ParameterHintsProvider = ParameterHintsProvider; + exports.JumpToDefProvider = JumpToDefProvider; + exports.LintingProvider = LintingProvider; +}); 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..abd0c9b41f5 --- /dev/null +++ b/src/languageTools/LanguageClientWrapper.js @@ -0,0 +1,627 @@ +/* + * 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 (params && 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); + }; + + //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); + }; + + //gotoDefinition + LanguageClientWrapper.prototype.gotoDefinition = function (params) { + return this._request(ToolingInfo.FEATURES.JUMP_TO_DEFINITION, params); + }; + + //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; + + //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..c4bbc7de898 --- /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[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..614be647561 --- /dev/null +++ b/src/languageTools/node/RegisterLanguageClientInfo.js @@ -0,0 +1,290 @@ +/* + * 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) { + 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 = {}; +} + +function init(domainManager) { + if (!domainManager.hasDomain(domainName)) { + domainManager.registerDomain(domainName, { + major: 0, + minor: 1 + }); + } + + domainManager.registerCommand( + domainName, + "initialize", + initialize, + false, + "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/root/strings.js b/src/nls/root/strings.js index 594a566764e..074c462c438 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -875,5 +875,8 @@ define({ "NUMBER_WITH_PERCENTAGE" : "{0}%", // Strings for Related Files - "CMD_FIND_RELATED_FILES" : "Find Related Files" + "CMD_FIND_RELATED_FILES" : "Find Related Files", + + //Strings for LanguageTools Preferences + LANGUAGE_TOOLS_PREFERENCES : "Preferences for Language Tools" }); 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); + }); + }); + }); + }); +});