diff --git a/CLAUDE.md b/CLAUDE.md index f127fea18c..011f189c80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,9 @@ - No trailing whitespace. - Use `const` and `let` instead of `var`. +## Build artifacts — do not hand-edit +- **`src/cacheManifest.json`** is a generated build artifact (gitignored, produced by `gulpfile.js/index.js`). It lists files + hashes for the service-worker cache. Never hand-edit or commit it — it is regenerated by the build, so edits are overwritten and won't be tracked anyway. When you add/remove/rename source files, just let the build regenerate it. + ## Translations / i18n - All user-visible strings must go in `src/nls/root/strings.js` — never hardcode English in source files. - Use `const Strings = require("strings");` then `Strings.KEY_NAME`. diff --git a/gulpfile.js/index.js b/gulpfile.js/index.js index 9a8ee23d3a..8d1b186de1 100644 --- a/gulpfile.js/index.js +++ b/gulpfile.js/index.js @@ -909,7 +909,7 @@ function _renameExtensionConcatAsExtensionJSInDist(extensionName) { } const minifyableExtensions = ["CloseOthers", "CodeFolding", "DebugCommands", "Git", - "HealthData", "JavaScriptCodeHints", "JavaScriptRefactoring", "QuickView"]; + "HealthData", "JavaScriptCodeHints", "JavaScriptRefactoring", "QuickView", "TypeScriptSupport"]; // extensions that nned not be minified either coz they are single file extensions or some other reason. const nonMinifyExtensions = ["CSSAtRuleCodeHints", "CSSCodeHints", "CSSPseudoSelectorHints", "DarkTheme", "HandlebarsSupport", "HTMLCodeHints", "HtmlEntityCodeHints", diff --git a/gulpfile.js/thirdparty-lib-copy.js b/gulpfile.js/thirdparty-lib-copy.js index e23579f6b3..cc064520a2 100644 --- a/gulpfile.js/thirdparty-lib-copy.js +++ b/gulpfile.js/thirdparty-lib-copy.js @@ -266,7 +266,10 @@ let copyThirdPartyLibs = series( renameFile.bind(renameFile, 'node_modules/@xterm/addon-webgl/lib/addon-webgl.js.map', 'addon-webgl.js.map', 'src/thirdparty/xterm'), copyLicence.bind(copyLicence, 'node_modules/@xterm/xterm/LICENSE', 'xterm') - + // vtsls language server (bundled in src-node is not installed in pipline tests as the destop app building + // does it for desktop LSP). we ran it once and copied the license here. + // copyLicence.bind(copyLicence, 'src-node/node_modules/@vtsls/language-server/LICENSE', 'vtsls'), + // copyLicence.bind(copyLicence, 'src-node/node_modules/typescript/LICENSE.txt', 'typescript') ); /** diff --git a/gulpfile.js/validate-build.js b/gulpfile.js/validate-build.js index 95417a7adf..d3911a7492 100644 --- a/gulpfile.js/validate-build.js +++ b/gulpfile.js/validate-build.js @@ -35,9 +35,12 @@ const LARGE_FILE_LIST_DEV = { // Size limits for production/staging builds (in MB) const PROD_MAX_FILE_SIZE_MB = 2; const PROD_MAX_TOTAL_SIZE_MB = 80; -// Custom size limits for known large files (size in MB) For staging/production builds +// Custom size limits for known large files (size in MB) For staging/production builds. +// Margin policy: keep ~1 MB of headroom over a file's current size for individual files (and ~5 MB +// for the aggregate/total). Bump a limit only enough to restore that margin when a file legitimately +// grows - don't pad it large, so unexpected size jumps still get caught. const LARGE_FILE_LIST_PROD = { - 'dist/brackets.js': 10, // this is the full minified file itself renamed in prod + 'dist/brackets.js': 11, // this is the full minified file itself renamed in prod (~10 MB + 1 MB margin) 'dist/phoenix/virtualfs.js.map': 3 }; diff --git a/src-node/index.js b/src-node/index.js index a40bc1c678..4ad5376e00 100644 --- a/src-node/index.js +++ b/src-node/index.js @@ -70,6 +70,9 @@ require("./test-connection"); require("./utils"); require("./terminal"); require("./git/cli"); +// Note: "./lsp-client" is intentionally NOT required here. It is lazy-loaded on demand the +// first time the desktop LSP client is used (via NodeUtils._loadNodeExtensionModule), so the +// LSP framework adds nothing to node boot time. See src/languageTools/LSPClient.js. require("./claude-code-agent"); function randomNonce(byteLength) { const randomBuffer = new Uint8Array(byteLength); diff --git a/src-node/lsp-client.js b/src-node/lsp-client.js new file mode 100644 index 0000000000..f8929ec9f2 --- /dev/null +++ b/src-node/lsp-client.js @@ -0,0 +1,344 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * Pluggable LSP Client Infrastructure (Node side) + * + * Provides a reusable Language Server Protocol (LSP) client that supports multiple + * language servers simultaneously via a serverId-based registry. Browser extensions only + * configure their language server and make high-level calls; this module owns the process + * spawning and JSON-RPC framing. + * + * ``` + * BROWSER (Phoenix) NODE (this module) + * TypeScript/Python/Rust ─NodeConnector──▶ lsp-client.js ──stdio──▶ vtsls / pylsp / rust-analyzer + * extensions ("ph-lsp") (spawn + JSON-RPC, Content-Length framing) + * ``` + * + * API (called from the browser via `lspConnector.execPeer(, params)`): + * - startServer({ serverId, command, args=['--stdio'], rootUri }) -> { success, serverId, pid } + * - sendRequest({ serverId, method, params }) -> LSP result (awaits response) + * - sendNotification({ serverId, method, params }) -> { success } (fire and forget) + * - stopServer({ serverId }) -> { success, serverId } + * - listServers() -> [{ serverId, pid, rootUri }] + * - ping() -> { status: "pong", activeServers } + * + * Events emitted to the browser (`lspConnector.on(, ...)`): + * - 'lspNotification' { serverId, method, params } (e.g. textDocument/publishDiagnostics) + * - 'serverExit' { serverId, code } + * - 'serverError' { serverId, error } + * + * Server resolution order when starting: `src-node/node_modules/.bin/` (bundled), + * then the system PATH. Messages use JSON-RPC 2.0 over stdio with Content-Length headers. + */ + +// Create connector at module load time (same pattern as src-node/git/cli.js) +const nodeConnector = global.createNodeConnector("ph-lsp", exports); + +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +// Path to node_modules/.bin for bundled LSP servers +const NODE_MODULES_BIN = path.join(__dirname, 'node_modules', '.bin'); + +// Registry of active servers: serverId -> serverState +const servers = new Map(); +let globalRequestId = 0; + +// Timeout for LSP requests (2 minutes) +const LSP_REQUEST_TIMEOUT = 120000; + +/** + * Encode a JSON-RPC message with an LSP Content-Length header. + * @param {Object} message - The JSON-RPC message object + * @returns {string} The encoded message with headers + */ +function encode(message) { + const content = JSON.stringify(message); + return `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n${content}`; +} + +/** + * Create a stream parser for LSP messages from a specific server. + * Uses Buffer operations because Content-Length is measured in bytes. + * @param {string} serverId - The server identifier + * @returns {Function} Parser function that processes incoming data chunks + */ +function createParser(serverId) { + let buffer = Buffer.alloc(0); + const HEADER_DELIMITER = Buffer.from('\r\n\r\n'); + + return (data) => { + buffer = Buffer.concat([buffer, data]); + + while (true) { + const headerEnd = buffer.indexOf(HEADER_DELIMITER); + if (headerEnd === -1) { + break; + } + + const header = buffer.slice(0, headerEnd).toString('utf8'); + const match = header.match(/Content-Length: (\d+)/i); + if (!match) { + // Invalid header - skip a byte and resync. + buffer = buffer.slice(1); + continue; + } + + const contentLength = parseInt(match[1], 10); + const contentStart = headerEnd + HEADER_DELIMITER.length; + + if (buffer.length < contentStart + contentLength) { + break; // Wait for more data. + } + + const json = buffer.slice(contentStart, contentStart + contentLength).toString('utf8'); + buffer = buffer.slice(contentStart + contentLength); + + try { + handleMessage(serverId, JSON.parse(json)); + } catch (e) { + console.error(`[lsp-client][${serverId}] parse error:`, e.message); + } + } + }; +} + +/** + * Handle a single incoming LSP message from a server. + * @param {string} serverId - The server identifier + * @param {Object} msg - The parsed JSON-RPC message + */ +function handleMessage(serverId, msg) { + const server = servers.get(serverId); + if (!server) { + return; + } + + if (msg.id !== undefined && server.pending.has(msg.id)) { + // Response to a request we sent. + const { resolve, reject } = server.pending.get(msg.id); + server.pending.delete(msg.id); + if (msg.error) { + reject(msg.error); + } else { + resolve(msg.result); + } + } else if (msg.method) { + // Notification or server-initiated request - forward to the browser. + nodeConnector.triggerPeer('lspNotification', { serverId, ...msg }); + } +} + +/** + * Ping endpoint to verify the LSP connector is alive. + * @returns {Promise} Status and list of active servers + */ +exports.ping = async function ping() { + return { status: "pong", activeServers: Array.from(servers.keys()) }; +}; + +/** + * Start a new language server. + * @param {Object} params - Server configuration + * @param {string} params.serverId - Unique identifier for this server instance + * @param {string} params.command - Command used to spawn the language server + * @param {string[]} [params.args=['--stdio']] - Arguments for the command + * @param {string} params.rootUri - Root URI of the workspace + * @returns {Promise} Result with success status and server info + */ +exports.startServer = async function startServer(params) { + const { serverId, command, args = ['--stdio'], rootUri } = params; + + if (!serverId || !command) { + throw new Error('serverId and command are required'); + } + + if (servers.has(serverId)) { + return { success: true, message: "already running", serverId }; + } + + // Prefer a server bundled in node_modules/.bin, otherwise fall back to PATH. + let commandPath = command; + const localBinPath = path.join(NODE_MODULES_BIN, command); + if (fs.existsSync(localBinPath)) { + commandPath = localBinPath; + } + + return new Promise((resolve, reject) => { + const serverProcess = spawn(commandPath, args, { stdio: ['pipe', 'pipe', 'pipe'] }); + const parser = createParser(serverId); + + const serverState = { + process: serverProcess, + pending: new Map(), + rootUri, + stderrTail: [] // keep the last few stderr lines to attach to crash reports + }; + + let hasResolved = false; + + serverProcess.stdout.on('data', parser); + serverProcess.stderr.on('data', (data) => { + const text = data.toString(); + serverState.stderrTail.push(text); + if (serverState.stderrTail.length > 50) { + serverState.stderrTail.shift(); + } + console.error(`[lsp-client][${serverId} stderr]`, text.trimEnd()); + }); + + serverProcess.on('spawn', () => { + servers.set(serverId, serverState); + hasResolved = true; + resolve({ success: true, serverId, pid: serverProcess.pid }); + }); + + serverProcess.on('exit', (code, signal) => { + servers.delete(serverId); + const stderr = serverState.stderrTail.join(''); + if (code) { + console.error(`[lsp-client][${serverId}] exited code=${code} signal=${signal || 'none'}`); + } + nodeConnector.triggerPeer('serverExit', { serverId, code, signal, stderr }); + if (!hasResolved) { + hasResolved = true; + reject(new Error(`Server ${serverId} exited immediately with code ${code}` + + (stderr ? `\n${stderr}` : ''))); + } + }); + + serverProcess.on('error', (err) => { + console.error(`[lsp-client][${serverId}] spawn error:`, err.message); + servers.delete(serverId); + nodeConnector.triggerPeer('serverError', { serverId, error: err.message }); + if (!hasResolved) { + hasResolved = true; + reject(new Error(`Failed to spawn ${serverId}: ${err.message}`)); + } + }); + + // Guard in case the 'spawn' event never fires. + setTimeout(() => { + if (!hasResolved) { + hasResolved = true; + reject(new Error(`Timeout waiting for ${serverId} to start`)); + } + }, 10000); + }); +}; + +/** + * Send an LSP request to a server and wait for the response. + * @param {Object} params - Request parameters + * @param {string} params.serverId - Target server identifier + * @param {string} params.method - LSP method name + * @param {Object} params.params - LSP request parameters + * @returns {Promise} The LSP response result + */ +exports.sendRequest = async function sendRequest(params) { + const { serverId, method, params: lspParams } = params; + const server = servers.get(serverId); + + if (!server) { + throw new Error(`Server ${serverId} not running`); + } + + const id = ++globalRequestId; + const msg = { jsonrpc: '2.0', id, method, params: lspParams }; + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + if (server.pending.has(id)) { + server.pending.delete(id); + reject(new Error(`Request ${method} timed out after ${LSP_REQUEST_TIMEOUT}ms`)); + } + }, LSP_REQUEST_TIMEOUT); + + server.pending.set(id, { + resolve: (result) => { + clearTimeout(timeoutId); + resolve(result); + }, + reject: (error) => { + clearTimeout(timeoutId); + reject(error); + } + }); + + server.process.stdin.write(encode(msg)); + }); +}; + +/** + * Send an LSP notification to a server (no response expected). + * @param {Object} params - Notification parameters + * @param {string} params.serverId - Target server identifier + * @param {string} params.method - LSP method name + * @param {Object} params.params - LSP notification parameters + * @returns {Promise} Success confirmation + */ +exports.sendNotification = async function sendNotification(params) { + const { serverId, method, params: lspParams } = params; + const server = servers.get(serverId); + + if (!server) { + throw new Error(`Server ${serverId} not running`); + } + + const msg = { jsonrpc: '2.0', method, params: lspParams }; + server.process.stdin.write(encode(msg)); + return { success: true }; +}; + +/** + * Stop a running language server. + * @param {Object} params - Stop parameters + * @param {string} params.serverId - Server identifier to stop + * @returns {Promise} Success confirmation + */ +exports.stopServer = async function stopServer(params) { + const { serverId } = params; + const server = servers.get(serverId); + + if (server) { + // Reject any in-flight requests so browser-side promises do not hang. + for (const { reject } of server.pending.values()) { + reject(new Error(`Server ${serverId} stopped`)); + } + server.pending.clear(); + server.process.kill(); + servers.delete(serverId); + } + return { success: true, serverId }; +}; + +/** + * List all active language servers. + * @returns {Promise} Array of server info objects + */ +exports.listServers = async function listServers() { + return Array.from(servers.entries()).map(([id, state]) => ({ + serverId: id, + pid: state.process.pid, + rootUri: state.rootUri + })); +}; diff --git a/src-node/package-lock.json b/src-node/package-lock.json index b65ecac73d..b97c6c92be 100644 --- a/src-node/package-lock.json +++ b/src-node/package-lock.json @@ -1,18 +1,19 @@ { "name": "@phcode/node-core", - "version": "5.1.21-0", + "version": "5.2.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phcode/node-core", - "version": "5.1.21-0", + "version": "5.2.0-0", "hasInstallScript": true, "license": "GNU-AGPL3.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.126", "@expo/sudo-prompt": "^9.3.2", "@phcode/fs": "^4.0.2", + "@vtsls/language-server": "^0.3.0", "cross-spawn": "^7.0.6", "lmdb": "^3.5.1", "mime-types": "^2.1.35", @@ -440,6 +441,54 @@ "ws": "^8.13.0" } }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "license": "MIT" + }, + "node_modules/@vtsls/language-server": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vtsls/language-server/-/language-server-0.3.0.tgz", + "integrity": "sha512-EYTkCHNGz3MFSP7z0DZ5+WBQY5CWEH7bCUu53EaDloBTjghoi2vfZqSrS0+7WsRG03MhBhjGG9ifNee/2kixvQ==", + "license": "MIT", + "dependencies": { + "@vtsls/language-service": "0.3.0", + "vscode-languageserver": "^9.0.1", + "vscode-uri": "^3.1.0" + }, + "bin": { + "vtsls": "bin/vtsls.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vtsls/language-service": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vtsls/language-service/-/language-service-0.3.0.tgz", + "integrity": "sha512-u2Z5oY64953CvG1SdI6Zimlbnaqj7OT/sj9zcw/gr/f6pl3e0ryh8lsa376MeC9T/SW7q+Aw0YMyUZIIIZOyng==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "@vtsls/vscode-fuzzy": "0.1.0", + "jsonc-parser": "^3.2.0", + "semver": "7.5.2", + "typescript": "5.9.3", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-uri": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vtsls/vscode-fuzzy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@vtsls/vscode-fuzzy/-/vscode-fuzzy-0.1.0.tgz", + "integrity": "sha512-jpJ6pFyi152BZ65j1D7otCf1YA9xaMqXV6nn2MOF8BNx0mkwIp2lTj26xvGk/mvEtiVvsGN3vMiBNXMxcv6bIA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1350,6 +1399,12 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/lmdb": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", @@ -1377,6 +1432,18 @@ "@lmdb/lmdb-win32-x64": "3.5.1" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3672,6 +3739,21 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -3930,6 +4012,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3948,6 +4043,80 @@ "node": ">= 0.8" } }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0.tgz", + "integrity": "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.0.tgz", + "integrity": "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.0", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", @@ -3994,6 +4163,12 @@ } } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/src-node/package.json b/src-node/package.json index 052a1f7051..24f0a1d0f3 100644 --- a/src-node/package.json +++ b/src-node/package.json @@ -23,6 +23,7 @@ "@anthropic-ai/claude-agent-sdk": "^0.2.126", "@expo/sudo-prompt": "^9.3.2", "@phcode/fs": "^4.0.2", + "@vtsls/language-server": "^0.3.0", "cross-spawn": "^7.0.6", "lmdb": "^3.5.1", "mime-types": "^2.1.35", diff --git a/src/brackets.js b/src/brackets.js index 3e427c081a..e6adcb0756 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -211,13 +211,8 @@ 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"); + // Language Tools (LSP) are desktop-only and loaded lazily by languageTools/LSPClient the + // first time a language server is started, so they are intentionally not required at boot. // web workers require("worker/IndexingWorker"); diff --git a/src/command/KeyBindingManager.js b/src/command/KeyBindingManager.js index 7da918786e..d1ea60e65e 100644 --- a/src/command/KeyBindingManager.js +++ b/src/command/KeyBindingManager.js @@ -553,6 +553,52 @@ define(function (require, exports, module) { * @private * @return {string} If the key is OS-inconsistent, the correct key; otherwise, the original key. **/ + // Maps a physical key code (event.code, which is keyboard-layout and input-method independent) + // to the key name used in keymaps. Used as a fallback when event.key is unusable. + const _CODE_TO_KEY = { + "Space": "Space", + "Enter": "Enter", + "NumpadEnter": "Enter", + "Tab": "Tab", + "Backspace": "Backspace", + "Delete": "Delete", + "Escape": "Esc", + "ArrowUp": "Up", + "ArrowDown": "Down", + "ArrowLeft": "Left", + "ArrowRight": "Right", + "Home": "Home", + "End": "End", + "PageUp": "PageUp", + "PageDown": "PageDown", + "Minus": "-", + "Equal": "=", + "BracketLeft": "[", + "BracketRight": "]", + "Backslash": "\\", + "Semicolon": ";", + "Quote": "'", + "Comma": ",", + "Period": ".", + "Slash": "/", + "Backquote": "`" + }; + function _mapCodeToKey(code) { + if (!code) { + return null; + } + if (/^Key[A-Z]$/.test(code)) { // KeyA -> A + return code.slice(3); + } + if (/^Digit[0-9]$/.test(code)) { // Digit1 -> 1 + return code.slice(5); + } + if (/^Numpad[0-9]$/.test(code)) { // Numpad1 -> 1 + return code.slice(6); + } + return _CODE_TO_KEY[code] || null; + } + function _mapKeycodeToKey(event) { // key code mapping https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values if((event.ctrlKey || event.metaKey) && event.altKey && brackets.platform === "mac"){ @@ -565,6 +611,15 @@ define(function (require, exports, module) { } } const key = event.key; + // Some input methods (e.g. IBus/fcitx on Linux intercepting Ctrl-Space) deliver the event + // with key === "Unidentified"/"Dead". Fall back to the physical key code so shortcuts like + // Ctrl-Space still resolve correctly. + if (key === "Unidentified" || key === "Dead" || !key) { + const fromCode = _mapCodeToKey(event.code); + if (fromCode) { + return fromCode; + } + } let codes = { "ArrowUp": "Up", "ArrowDown": "Down", diff --git a/src/editor/CodeHintList.js b/src/editor/CodeHintList.js index d50095fcff..4c86a4eb5f 100644 --- a/src/editor/CodeHintList.js +++ b/src/editor/CodeHintList.js @@ -135,16 +135,24 @@ define(function (require, exports, module) { * @param {{source: "KeyboardNav", event}} reason optional reason for selection change */ CodeHintList.prototype._setSelectedIndex = function (index, reason) { - var items = this.$hintMenu.find("li"); + var items = this.$hintMenu.find("li.code-hints-list-item"); // Range check index = Math.max(-1, Math.min(index, items.length - 1)); - // Clear old highlight - if (this.selectedIndex !== -1) { - $(items[this.selectedIndex]).find("a").removeClass("highlight"); + // Track whether the user has actively navigated (arrow keys) since the current query was + // issued. Used by update() to decide whether to preserve the selection across an async + // rebuild (preserve only if the user navigated; otherwise pick the best match). + if (reason && reason.source === SELECTION_REASON.KEYBOARD_NAV) { + this._userNavigated = true; } + // Clear old highlight. Clear ALL highlighted items (not just items[selectedIndex]) so a + // single-selection invariant is always enforced - the index can get out of sync with the + // DOM when the list is rebuilt by async providers, which would otherwise leave multiple + // rows looking selected. + items.find("a.highlight").removeClass("highlight"); + this.selectedIndex = index; // Highlight the new selected item, if necessary @@ -215,7 +223,7 @@ define(function (require, exports, module) { } // clear the list - this.$hintMenu.find("li").remove(); + this.$hintMenu.find("li.code-hints-list-item").remove(); // if there are no hints then close the list; otherwise add them and // set the selection @@ -392,7 +400,7 @@ define(function (require, exports, module) { // Calculate the number of items per scroll page. function _itemsPerPage() { var itemsPerPage = 1, - $items = self.$hintMenu.find("li"), + $items = self.$hintMenu.find("li.code-hints-list-item"), $view = self.$hintMenu.find("ul.dropdown-menu"), itemHeight; @@ -465,7 +473,7 @@ define(function (require, exports, module) { } // Trigger a click handler to commmit the selected item - $(this.$hintMenu.find("li")[this.selectedIndex]).trigger("click"); + $(this.$hintMenu.find("li.code-hints-list-item")[this.selectedIndex]).trigger("click"); } else { // Let the event bubble. return false; @@ -528,8 +536,34 @@ define(function (require, exports, module) { */ CodeHintList.prototype.update = function (hintObj) { this.$hintMenu.addClass("apply-transition"); + + // Preserve the user's current selection across the rebuild ONLY if they actively navigated + // since this query was issued. Without the guard, plain typing (a new query) would keep a + // stale selection instead of selecting the best match; with it, a late async response that + // arrives while the user is arrow-navigating won't reset their position. + var prevText = null; + if (this._userNavigated && this.selectedIndex >= 0) { + var $prevSel = this.$hintMenu.find("li.code-hints-list-item").eq(this.selectedIndex).find(".brackets-hints").first(); + if ($prevSel.length) { + prevText = $prevSel.text(); + } + } + this._buildListView(hintObj); + if (prevText) { + var matchIndex = -1; + this.$hintMenu.find("li.code-hints-list-item").each(function (i, li) { + if ($(li).find(".brackets-hints").first().text() === prevText) { + matchIndex = i; + return false; + } + }); + if (matchIndex >= 0) { + this._setSelectedIndex(matchIndex); + } + } + // Update the CodeHintList location if (this.hints.length) { var hintPos = this._calcHintListLocation(); diff --git a/src/editor/CodeHintManager.js b/src/editor/CodeHintManager.js index aec0c311da..a919b60bce 100644 --- a/src/editor/CodeHintManager.js +++ b/src/editor/CodeHintManager.js @@ -450,6 +450,12 @@ define(function (require, exports, module) { return hintList.callMoveUp(callMoveUpEvent); } + // A new query is being issued; clear the "user navigated" flag so the upcoming (possibly + // async) result selects the best match, unless the user navigates again before it arrives. + if (hintList) { + hintList._userNavigated = false; + } + var response = null; // Get hints from regular provider if available @@ -540,6 +546,13 @@ define(function (require, exports, module) { return; } + // Foolproof: never allow two concurrent hint lists. If a session somehow still exists, + // close it (removing its menu + global keydown hook) before starting a new one. Otherwise + // the old list would leak and both would react to Up/Down/Enter. + if (hintList) { + _endSession(); + } + // Don't start a session if we have a multiple selection. if (editor.getSelections().length > 1) { return; diff --git a/src/editor/TabstopManager.js b/src/editor/TabstopManager.js new file mode 100644 index 0000000000..ee4907d5fa --- /dev/null +++ b/src/editor/TabstopManager.js @@ -0,0 +1,341 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://www.gnu.org/licenses/. + * + */ + +/** + * TabstopManager - a small, reusable engine for inserting "snippet" text that carries tab-stops and + * placeholders, and for letting the user cycle through those stops with Tab / Shift-Tab. + * + * It understands the LSP snippet grammar (which is the same one VS Code, Emmet and TextMate use): + * - `$1`, `$2`, ... ordered tab-stops, `$0` the final caret + * - `${1:placeholder}` a tab-stop pre-filled with default text that gets selected for type-over + * - `${1|a,b,c|}` a choice (we take the first option) + * - `${VAR}` / `${VAR:default}` variables (unresolved vars contribute nothing / their default) + * - the escapes `\$`, `\}`, `\\` + * + * Two ways to use it: + * - `parseSnippet(text)` -> `{ text, stops }` for callers that only need the expanded plain text + * and the stop offsets (e.g. to place a single caret themselves). + * - `insertSnippet(editor, text, from, to)` -> expands, inserts over [from, to], selects the first + * stop, and (when there is more than one stop) starts a Tab-navigable session backed by markers + * so the stops follow any later edits (e.g. an auto-import line inserted above). + * + * NOTE: this is currently wired only into the LSP completion path (languageTools/DefaultProviders). + * The Emmet expander (HTMLCodeHints) and the custom-snippets feature have their own stable cursor + * handling and were intentionally left untouched; they can migrate onto this manager in future. + */ +define(function (require, exports, module) { + + /** + * Expand an LSP snippet into plain text plus the list of tab-stops. + * @param {string} snippet + * @return {{text: string, stops: Array<{number: number, start: number, end: number}>}} + * `stops` holds one entry per distinct stop number (first occurrence), with offsets into + * `text`; positive numbers come first in ascending order, then `$0` last. + */ + function parseSnippet(snippet) { + var out = "", + i = 0, + len = snippet.length, + stops = {}; // number -> { start, end } (first occurrence wins) + function record(num, start, end) { + if (!(num in stops)) { + stops[num] = { start: start, end: end }; + } + } + function readBraceBody(braceIdx) { + // braceIdx points at "{"; returns { inner, next } honoring nesting and escapes. + var j = braceIdx + 1, + depth = 1, + inner = ""; + while (j < len && depth > 0) { + var c = snippet.charAt(j); + if (c === "\\" && j + 1 < len) { + inner += snippet.charAt(j + 1); + j += 2; + continue; + } + if (c === "{") { + depth++; + inner += c; + j++; + continue; + } + if (c === "}") { + depth--; + if (depth === 0) { + j++; + break; + } + inner += c; + j++; + continue; + } + inner += c; + j++; + } + return { inner: inner, next: j }; + } + while (i < len) { + var ch = snippet.charAt(i); + if (ch === "\\" && i + 1 < len) { + var esc = snippet.charAt(i + 1); + if (esc === "$" || esc === "}" || esc === "\\") { + out += esc; + i += 2; + continue; + } + out += ch; + i++; + continue; + } + if (ch === "$") { + var simple = /^\$(\d+)/.exec(snippet.slice(i)); + if (simple) { + record(parseInt(simple[1], 10), out.length, out.length); + i += simple[0].length; + continue; + } + if (snippet.charAt(i + 1) === "{") { + var body = readBraceBody(i + 1); + i = body.next; + var placeholder = /^(\d+):([\s\S]*)$/.exec(body.inner), + choice = /^(\d+)\|([\s\S]*)\|$/.exec(body.inner), + plain = /^(\d+)$/.exec(body.inner), + varDefault = /^[A-Za-z_][A-Za-z0-9_]*:([\s\S]*)$/.exec(body.inner); + if (placeholder) { + var sub = parseSnippet(placeholder[2]), + phStart = out.length; + out += sub.text; + record(parseInt(placeholder[1], 10), phStart, out.length); + // Preserve any tab-stops nested inside the placeholder default (e.g. + // ${1:a ${2:b} c}), shifted into this snippet's coordinate space. + for (var si = 0; si < sub.stops.length; si++) { + record(sub.stops[si].number, + phStart + sub.stops[si].start, phStart + sub.stops[si].end); + } + } else if (choice) { + var first = choice[2].split(",")[0] || "", + chStart = out.length; + out += first; + record(parseInt(choice[1], 10), chStart, out.length); + } else if (plain) { + record(parseInt(plain[1], 10), out.length, out.length); + } else if (varDefault) { + // Unknown variable with a default: emit the default (we don't resolve vars). + out += parseSnippet(varDefault[1]).text; + } + // A bare `${VAR}` we can't resolve contributes nothing. + continue; + } + var varName = /^\$[A-Za-z_][A-Za-z0-9_]*/.exec(snippet.slice(i)); + if (varName) { + i += varName[0].length; + continue; + } + out += ch; + i++; + continue; + } + out += ch; + i++; + } + // Order stops: positive numbers ascending, then $0 (the final caret) last. + var ordered = Object.keys(stops).map(Number).filter(function (x) { + return x > 0; + }).sort(function (a, b) { + return a - b; + }); + if (0 in stops) { + ordered.push(0); + } + return { + text: out, + stops: ordered.map(function (num) { + return { number: num, start: stops[num].start, end: stops[num].end }; + }) + }; + } + + // ---- Tab-navigation session ---------------------------------------------------------------- + + var _session = null; // { editor, markers: [marker], index, keymap } + + function _clearSession() { + if (!_session) { + return; + } + var session = _session; + _session = null; // null first so the handlers below become no-ops if re-entered + session.markers.forEach(function (m) { + m.clear(); + }); + session.editor._codeMirror.removeKeyMap(session.keymap); + session.editor.off(".tabstop"); + } + + /** + * Resolve a marker (markText range or bookmark) to a {from, to} document range, or null if the + * marker no longer exists in the document. + */ + function _markerRange(marker) { + var found = marker.find(); + if (!found) { + return null; + } + // markText -> {from, to}; setBookmark -> a single position + if (found.from && found.to) { + return { from: found.from, to: found.to }; + } + return { from: found, to: found }; + } + + function _selectStop(index) { + if (!_session || index < 0 || index >= _session.markers.length) { + return false; + } + var range = _markerRange(_session.markers[index]); + if (!range) { + return false; + } + _session.index = index; + _session.editor.setSelection(range.from, range.to); + return true; + } + + function _gotoNext() { + // Move forward through the stops; leaving the last one ends the session (caret stays put). + for (var i = _session.index + 1; i < _session.markers.length; i++) { + if (_selectStop(i)) { + if (i === _session.markers.length - 1) { + // landed on the final stop ($0 or the last placeholder) - nothing more to visit + _clearSession(); + } + return; + } + } + _clearSession(); + } + + function _gotoPrev() { + for (var i = _session.index - 1; i >= 0; i--) { + if (_selectStop(i)) { + return; + } + } + } + + /** + * Expand a snippet, insert it over the given range, and place the caret at the first tab-stop. + * When the snippet has more than one stop, a Tab / Shift-Tab navigable session is started; the + * stops are tracked with markers so they follow any subsequent edits (e.g. an auto-import line + * inserted above the snippet). Any previously active session is ended first. + * + * @param {Editor} editor + * @param {string} snippet - raw snippet text (LSP snippet grammar) + * @param {{line: number, ch: number}} from - start of the range to replace + * @param {{line: number, ch: number}} to - end of the range to replace + * @return {{text: string, stops: Array}} the parsed snippet (already inserted) + */ + function insertSnippet(editor, snippet, from, to) { + _clearSession(); + var parsed = parseSnippet(snippet); + editor.document.replaceRange(parsed.text, from, to); + + // Translate an offset within the inserted text (which may contain newlines) to a document + // position relative to `from`. + function posFromOffset(off) { + var seg = parsed.text.slice(0, off).split("\n"); + if (seg.length === 1) { + return { line: from.line, ch: from.ch + off }; + } + return { line: from.line + seg.length - 1, ch: seg[seg.length - 1].length }; + } + + if (!parsed.stops.length) { + editor.setCursorPos(posFromOffset(parsed.text.length).line, posFromOffset(parsed.text.length).ch); + return parsed; + } + + // Single stop: just place the caret / select the placeholder, no navigation session needed. + if (parsed.stops.length === 1) { + var only = parsed.stops[0], + s = posFromOffset(only.start), + e = posFromOffset(only.end); + if (s.line === e.line && s.ch === e.ch) { + editor.setCursorPos(s.line, s.ch); + } else { + editor.setSelection(s, e); + } + return parsed; + } + + // Multiple stops: lay down markers and start a Tab-navigable session. + var markers = parsed.stops.map(function (stop) { + var ms = posFromOffset(stop.start), + me = posFromOffset(stop.end); + if (ms.line === me.line && ms.ch === me.ch) { + return editor.setBookmark("tabstop", ms, { insertLeft: true }); + } + return editor.markText("tabstop", ms, me, { + clearWhenEmpty: false, + inclusiveLeft: false, + inclusiveRight: true + }); + }); + + var keymap = { + "name": "tabstop-session", + "Tab": function () { + _gotoNext(); + }, + "Shift-Tab": function () { + _gotoPrev(); + }, + "Esc": function () { + _clearSession(); + } + }; + + _session = { editor: editor, markers: markers, index: -1, keymap: keymap }; + editor._codeMirror.addKeyMap(keymap); + // End the session if the editor it belongs to is destroyed (file closed). Namespaced so + // _clearSession can remove it with a single off(".tabstop"). + editor.on("beforeDestroy.tabstop", _clearSession); + + _selectStop(0); + return parsed; + } + + /** + * @return {boolean} true while a Tab-navigable snippet session is active. + */ + function hasActiveSession() { + return !!_session; + } + + /** End any active session (caret is left wherever it currently is). */ + function endSession() { + _clearSession(); + } + + exports.parseSnippet = parseSnippet; + exports.insertSnippet = insertSnippet; + exports.hasActiveSession = hasActiveSession; + exports.endSession = endSession; +}); diff --git a/src/extensions/default/DefaultExtensions.json b/src/extensions/default/DefaultExtensions.json index 665ade6d0a..b1d9deb531 100644 --- a/src/extensions/default/DefaultExtensions.json +++ b/src/extensions/default/DefaultExtensions.json @@ -26,7 +26,8 @@ "HealthData" ], "desktopOnly": [ - "Git" + "Git", + "TypeScriptSupport" ], "warnExtensionStoreExtensions": { "description": "list extension ids here that you want to show this warning in extension store: 'You may not need this extension. Phoenix comes built in with this feature.'", diff --git a/src/extensions/default/HTMLCodeHints/main.js b/src/extensions/default/HTMLCodeHints/main.js index b7ca925030..ccbee8c02a 100644 --- a/src/extensions/default/HTMLCodeHints/main.js +++ b/src/extensions/default/HTMLCodeHints/main.js @@ -357,6 +357,10 @@ define(function (require, exports, module) { * Find the position where cursor should be placed after expansion * Looks for patterns like '><', '""', '' * + * NOTE: this is bespoke, stable cursor-placement logic for Emmet. `editor/TabstopManager` + * (added for the LSP completion path) is a reusable snippet/tab-stop engine that could replace + * this in future, giving Emmet real $1/${1:x}/$0 tab-stops with Tab navigation. + * * @param {Editor} editor - The editor instance * @param {String} indentedAbbr - the indented abbreviation * @param {Object} startPos - Starting position {line, ch} of the expansion diff --git a/src/extensions/default/TypeScriptSupport/main.js b/src/extensions/default/TypeScriptSupport/main.js new file mode 100644 index 0000000000..16e5b11032 --- /dev/null +++ b/src/extensions/default/TypeScriptSupport/main.js @@ -0,0 +1,293 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * TypeScript / JavaScript language support via the bundled `vtsls` language server. + * + * This extension is intentionally thin: all the heavy lifting lives in the shared + * `languageTools/LSPClient` module, which it loads lazily (only on desktop, only once Node is + * ready) so it never slows down boot. It just declares which languages map to which server and + * what initialization options vtsls needs. + */ +/*global path*/ +define(function (require, exports, module) { + + + const AppInit = brackets.getModule("utils/AppInit"), + ProjectManager = brackets.getModule("project/ProjectManager"), + DocumentManager = brackets.getModule("document/DocumentManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + NodeConnector = brackets.getModule("NodeConnector"); + + const SERVER_ID = "typescript"; + const SUPPORTED_LANGUAGES = ["javascript", "typescript", "jsx", "tsx"]; + + // Phoenix language id -> LSP languageId + const LANGUAGE_ID_MAP = { + javascript: "javascript", + typescript: "typescript", + jsx: "javascriptreact", + tsx: "typescriptreact" + }; + + // vtsls-specific initialization options (mirrors the configuration Zed/VS Code use). + const INITIALIZATION_OPTIONS = { + vtsls: { + experimental: { + completion: { + enableServerSideFuzzyMatch: true, + entriesLimit: 5000 + } + }, + autoUseWorkspaceTsdk: true + } + }; + + // --- "implicit any" diagnostics gating for plain JavaScript ----------------------------------- + // + // tsserver runs its language service over JS too, and emits the "noImplicitAny" family of + // diagnostics - including 7016 "Could not find a declaration file for module ... implicitly has + // an 'any' type. Try `npm i --save-dev @types/...`". For a pure-JS developer who never opted + // into type-checking, these are noise, so we suppress them for javascript/jsx UNLESS the project + // opts into type-checking via `checkJs` (tsconfig/jsconfig) or a per-file `// @ts-check`. This + // mirrors how VS Code only surfaces JS type diagnostics once you opt in. Real errors/warnings, + // unused-symbol/deprecation hints, and all type *intelligence* (hover/completion) are untouched. + const IMPLICIT_ANY_CODES = new Set([ + 7005, 7006, 7008, 7009, 7010, 7011, 7015, 7016, 7017, 7018, 7019, + 7022, 7023, 7024, 7025, 7026, 7031, 7033, 7034 + ]); + const SUPPRESS_LANGUAGES = ["javascript", "jsx"]; + const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"]; + + // Whether the current project opts into type-checking its JS (compilerOptions.checkJs). + let projectChecksJs = false; + + /** + * Strip JSONC comments and trailing commas so tsconfig/jsconfig can be JSON.parse'd. Good enough + * for reading a flag (does not handle `//` inside string values - rare in these configs). + * @param {string} str + * @return {string} + */ + function _stripJsonComments(str) { + str = str || ""; + str = str.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\//g, ""); // block comments + str = str.replace(/\/\/[^\n\r]*/g, ""); // line comments + str = str.replace(/,(\s*[}\]])/g, "$1"); // trailing commas + return str; + } + + /** + * Read tsconfig.json/jsconfig.json at the project root and resolve whether compilerOptions.checkJs + * is enabled. Does not follow `extends` (a project that only inherits checkJs from a base config + * is rare; can be added later). Mirrors the simple root-config reads ESLint/JSHint do. + * @return {Promise} + */ + function _detectProjectCheckJs() { + const root = ProjectManager.getProjectRoot(); + if (!root) { + return Promise.resolve(false); + } + const rootPath = root.fullPath; + return Promise.all(TS_CONFIG_FILES.map(function (name) { + return new Promise(function (resolve) { + FileSystem.getFileForPath(path.join(rootPath, name)).read(function (err, content) { + if (err || !content) { + resolve(false); + return; + } + try { + const cfg = JSON.parse(_stripJsonComments(content)); + resolve(!!(cfg && cfg.compilerOptions && cfg.compilerOptions.checkJs)); + } catch (e) { + resolve(false); + } + }); + }); + })).then(function (results) { + return results.indexOf(true) !== -1; + }); + } + + function _refreshCheckJs() { + const scanningRoot = ProjectManager.getProjectRoot() && ProjectManager.getProjectRoot().fullPath; + _detectProjectCheckJs().then(function (checks) { + // Ignore a stale result if the project switched while we were reading. + const nowRoot = ProjectManager.getProjectRoot() && ProjectManager.getProjectRoot().fullPath; + if (scanningRoot === nowRoot) { + projectChecksJs = checks; + } + }); + } + + /** + * True if an open JS file opts into type-checking with a leading `// @ts-check` (and not + * `// @ts-nocheck`). Only checks already-open documents - diagnostics are virtually always for + * the file being edited. + * @param {string} filePath + * @return {boolean} + */ + function _fileHasTsCheck(filePath) { + const doc = DocumentManager.getOpenDocumentForPath(filePath); + if (!doc) { + return false; + } + const head = doc.getText().slice(0, 1000); + if (/@ts-nocheck\b/.test(head)) { + return false; + } + return /@ts-check\b/.test(head); + } + + /** + * Drop "implicit any" diagnostics for plain JS/JSX files that haven't opted into type-checking. + * @param {Array} diagnostics - raw LSP diagnostics + * @param {{languageId:string, filePath:string}} ctx + * @return {Array} + */ + function filterDiagnostics(diagnostics, ctx) { + if (SUPPRESS_LANGUAGES.indexOf(ctx.languageId) === -1) { + return diagnostics; // typescript/tsx (or anything else) - never filtered + } + if (projectChecksJs || _fileHasTsCheck(ctx.filePath)) { + return diagnostics; // opted into typed JS - keep everything + } + return diagnostics.filter(function (d) { + const code = (typeof d.code === "string") ? parseInt(d.code, 10) : d.code; + return !IMPLICIT_ANY_CODES.has(code); + }); + } + + let registered = false; + let lspClientPromise = null; + + /** + * Asynchronously load the shared LSP framework on demand (keeps boot fast - these modules + * are not part of the boot dependency graph). Memoized; retries once to ride out any + * module-load race during startup. + * @return {Promise} the languageTools/LSPClient module + */ + function loadLSPClient() { + if (!lspClientPromise) { + lspClientPromise = new Promise(function (resolve, reject) { + brackets.getModule(["languageTools/LSPClient"], resolve, function () { + // Retry once - clear the require error state and try again on next tick. + setTimeout(function () { + brackets.getModule(["languageTools/LSPClient"], resolve, reject); + }, 500); + }); + }); + } + return lspClientPromise; + } + + /** + * LSP only runs in the desktop app where the Node engine is available. + * @return {boolean} + */ + function canRun() { + return typeof Phoenix !== "undefined" && Phoenix.isNativeApp && + NodeConnector.isNodeAvailable && NodeConnector.isNodeAvailable(); + } + + /** + * Resolve once the Node engine is ready (it is started lazily after boot). + * @param {number} timeout - max time to wait in ms + * @return {Promise} + */ + function waitForNodeReady(timeout) { + return new Promise(function (resolve) { + const deadline = Date.now() + timeout; + (function check() { + if (NodeConnector.isNodeReady()) { + resolve(true); + } else if (Date.now() > deadline) { + resolve(false); + } else { + setTimeout(check, 300); + } + }()); + }); + } + + async function start() { + if (registered || !canRun()) { + return; + } + const ready = await waitForNodeReady(30000); + if (!ready) { + console.error("[TypeScriptSupport] Node not ready - LSP disabled"); + return; + } + // Lazy-load the LSP framework only when we actually need it. + const LSPClient = await loadLSPClient(); + const client = await LSPClient.registerLanguageServer({ + serverId: SERVER_ID, + command: "vtsls", + args: ["--stdio"], + languages: SUPPORTED_LANGUAGES, + languageIdMap: LANGUAGE_ID_MAP, + initializationOptions: INITIALIZATION_OPTIONS, + filterDiagnostics: filterDiagnostics + }); + if (client) { + registered = true; + } + } + + // Begin loading the LSP framework as soon as the (desktop-only) extension loads - the + // reliable moment for module loading - so it is ready by the time start() runs. + if (canRun()) { + loadLSPClient(); + } + + AppInit.appReady(function () { + if (!canRun()) { + return; + } + _refreshCheckJs(); + start().catch(function (err) { + console.error("[TypeScriptSupport] init failed", err && (err.message || err)); + }).finally(function () { + // Signal for integration tests that the server start has been attempted/settled. + window._TypeScriptSupportReadyToIntegTest = true; + }); + + // Restart the server against the new workspace root when the project changes, and + // re-evaluate whether the new project type-checks its JS. + ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, function () { + _refreshCheckJs(); + if (registered) { + loadLSPClient().then(function (LSPClient) { + LSPClient.restartLanguageServer(SERVER_ID); + }); + } + }); + + // Pick up a tsconfig/jsconfig being added, edited, or removed at the project root. + ProjectManager.on(ProjectManager.EVENT_PROJECT_CHANGED_OR_RENAMED_PATH, function (_evt, changedPath) { + const root = ProjectManager.getProjectRoot(); + if (root && TS_CONFIG_FILES.some(function (name) { + return changedPath === path.join(root.fullPath, name); + })) { + _refreshCheckJs(); + } + }); + }); +}); diff --git a/src/extensions/default/TypeScriptSupport/package.json b/src/extensions/default/TypeScriptSupport/package.json new file mode 100644 index 0000000000..3059f2815e --- /dev/null +++ b/src/extensions/default/TypeScriptSupport/package.json @@ -0,0 +1,9 @@ +{ + "name": "typescript-support", + "title": "TypeScript/JavaScript Language Support", + "description": "Provides TypeScript and JavaScript language intelligence (completion, hover, jump-to-definition, signature help, diagnostics and find-references) via the bundled vtsls language server. Desktop only.", + "version": "1.0.0", + "engines": { + "brackets": ">=2.0.0" + } +} diff --git a/src/extensions/default/TypeScriptSupport/unittests.js b/src/extensions/default/TypeScriptSupport/unittests.js new file mode 100644 index 0000000000..a2a417e6fc --- /dev/null +++ b/src/extensions/default/TypeScriptSupport/unittests.js @@ -0,0 +1,189 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global describe, it, expect, beforeAll, afterAll, awaitsFor, awaitsForDone */ + +define(function (require, exports, module) { + + const SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"); + + const IMPLICIT_ANY_MESSAGE = "implicitly has an 'any' type"; + + describe("integration:TypeScript LSP", function () { + const testRootSpec = "/spec/TypeScriptSupport-test-files/"; + let testFolder = SpecRunnerUtils.getTestPath(testRootSpec), + testWindow, + $, + EditorManager, + CommandManager, + Commands, + CodeInspection, + QuickViewManager; + + // The LSP runs only in the desktop app (it spawns the vtsls Node process), so these tests + // are meaningless in the browser build - register a single skipped placeholder and bail. + if (!Phoenix.isNativeApp) { + it("is desktop-only - skipped in the browser build", function () { + expect(Phoenix.isNativeApp).toBeFalsy(); + }); + return; + } + + beforeAll(async function () { + testWindow = await SpecRunnerUtils.createTestWindowAndRun(); + $ = testWindow.$; + EditorManager = testWindow.brackets.test.EditorManager; + CommandManager = testWindow.brackets.test.CommandManager; + Commands = testWindow.brackets.test.Commands; + CodeInspection = testWindow.brackets.test.CodeInspection; + QuickViewManager = testWindow.brackets.getModule("features/QuickViewManager"); + CodeInspection.toggleEnabled(true); + // Wait until the extension has attempted to start the language server. + await awaitsFor(function () { + return testWindow._TypeScriptSupportReadyToIntegTest; + }, "TypeScript LSP server to start", 30000); + }, 30000); + + afterAll(async function () { + testWindow = null; + $ = null; + EditorManager = null; + CommandManager = null; + Commands = null; + CodeInspection = null; + await SpecRunnerUtils.closeTestWindow(); + }, 30000); + + function panelText() { + return $("#problems-panel").text(); + } + + async function _openInProject(subFolder, fileName) { + await SpecRunnerUtils.loadProjectInTestWindow(testFolder + subFolder); + await awaitsForDone(SpecRunnerUtils.openProjectFiles([fileName]), "open " + fileName); + } + + it("should report TypeScript type errors from the language server", async function () { + await _openInProject("ts/", "type-error.ts"); + // type-error.ts assigns a string to a `number` -> TS2322 "... not assignable ...". + await awaitsFor(function () { + return panelText().includes("not assignable"); + }, "TypeScript type error to be reported", 20000); + }, 30000); + + it("should report implicit-any in a JS project that opts into checkJs", async function () { + // js-checkjs has a jsconfig.json with checkJs + noImplicitAny, so the untyped parameter + // in implicit.js IS flagged - and our diagnostic filter keeps it (the project opted in). + await _openInProject("js-checkjs/", "implicit.js"); + await awaitsFor(function () { + return panelText().includes(IMPLICIT_ANY_MESSAGE); + }, "implicit-any to be reported under checkJs", 20000); + }, 30000); + + it("should NOT report implicit-any in a plain JS project", async function () { + // Precondition: confirm the server actually produces implicit-any for this exact code + // (under checkJs), so the plain-project assertion below reflects gating, not just timing. + await _openInProject("js-checkjs/", "implicit.js"); + await awaitsFor(function () { + return panelText().includes(IMPLICIT_ANY_MESSAGE); + }, "implicit-any under checkJs (precondition)", 20000); + + // Same code in a plain JS project (no jsconfig / no @ts-check): the "go add types" nag + // must not appear. Wait for inspection to settle clean, then assert it is absent. + await _openInProject("js-plain/", "implicit.js"); + await awaitsFor(function () { + return $("#status-inspection").hasClass("inspection-valid"); + }, "plain JS inspection to settle with no problems", 20000); + expect(panelText().includes(IMPLICIT_ANY_MESSAGE)).toBe(false); + }, 30000); + + // ----- hover quick-actions (Go to Definition / Find Usages) ------------------------------- + + // Query the hover popover at a position the same way QuickViewManager does internally. + async function _hoverPopoverAt(editor, line, ch) { + const pos = { line: line, ch: ch }; + const token = editor._codeMirror.getTokenAt(pos, true); + return QuickViewManager._queryPreviewProviders(editor, pos, token); + } + + // sample.ts/sample.js: greetUser is declared on line 1 and called on lines 5 and 6. Each + // lives in its own project folder so the (identically named) symbols don't collide in the + // server's inferred project. + const DECL_LINE = 1, CALL_LINE = 5, CALL_CH = 4; + + [{ ext: "ts", folder: "hover-ts/", file: "sample.ts" }, + { ext: "js", folder: "hover-js/", file: "sample.js" }].forEach(function (tc) { + + it("hover shows quick actions and Go to Definition navigates (" + tc.ext + ")", async function () { + await _openInProject(tc.folder, tc.file); + const editor = EditorManager.getCurrentFullEditor(); + let popover = null; + await awaitsFor(async function () { + popover = await _hoverPopoverAt(editor, CALL_LINE, CALL_CH); + return !!(popover && popover.content && popover.content.find(".lsp-hover-action").length === 2); + }, "hover quick actions to appear", 20000); + + const labels = popover.content.find(".lsp-hover-action-label").map(function () { + return $(this).text(); + }).get(); + expect(labels).toEqual(["Go to Definition", "Find Usages"]); + + // Click "Go to Definition" to jump from the call (line 5) to the declaration (line 1). + // Re-click through the hover until it takes effect - the server may still be indexing + // right after the project (re)opened, so an early click can be a no-op. + await awaitsFor(async function () { + if (EditorManager.getCurrentFullEditor().getCursorPos().line === DECL_LINE) { + return true; + } + const pop = await _hoverPopoverAt(editor, CALL_LINE, CALL_CH); + const $act = pop && pop.content && pop.content.find(".lsp-hover-action").eq(0); + if ($act && $act.length) { + $act.trigger("click"); + } + return EditorManager.getCurrentFullEditor().getCursorPos().line === DECL_LINE; + }, "Go to Definition to navigate to the declaration", 25000); + expect(EditorManager.getCurrentFullEditor().getCursorPos().line).toBe(DECL_LINE); + }, 40000); + + it("hover Find Usages opens the references panel (" + tc.ext + ")", async function () { + await _openInProject(tc.folder, tc.file); + const editor = EditorManager.getCurrentFullEditor(); + await awaitsFor(async function () { + return !!(await _hoverPopoverAt(editor, CALL_LINE, CALL_CH)); + }, "hover popover to be available", 20000); + + // "Find Usages" is the right-aligned action; clicking it opens the references panel. + // Retry through the hover until the panel opens (the server may still be indexing). + await awaitsFor(async function () { + if ($("#reference-in-files-results").is(":visible")) { + return true; + } + const pop = await _hoverPopoverAt(editor, CALL_LINE, CALL_CH); + const $end = pop && pop.content && pop.content.find(".lsp-hover-action--end"); + if ($end && $end.length) { + $end.trigger("click"); + } + return $("#reference-in-files-results").is(":visible"); + }, "references panel to open", 25000); + expect($("#reference-in-files-results").is(":visible")).toBe(true); + }, 40000); + }); + }); +}); diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js index 2cb213c871..872f0c28a7 100644 --- a/src/features/FindReferencesManager.js +++ b/src/features/FindReferencesManager.js @@ -36,11 +36,22 @@ define(function (require, exports, module) { Strings = require("strings"); var _providerRegistrationHandler = new ProviderRegistrationHandler(), - registerFindReferencesProvider = _providerRegistrationHandler.registerProvider.bind( - _providerRegistrationHandler - ), removeFindReferencesProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + /** + * Register a find-references provider. The command's enabled state is normally computed on file + * switch; a provider can register *after* the active file is already open (e.g. an LSP server + * that starts asynchronously at app launch), so re-evaluate the menu state here too - otherwise + * "Find All References" stays disabled until the user switches files. + * @param {Object} providerInfo + * @param {Array} languageIds + * @param {?number} priority + */ + function registerFindReferencesProvider(providerInfo, languageIds, priority) { + _providerRegistrationHandler.registerProvider(providerInfo, languageIds, priority); + setMenuItemStateForLanguage(); + } + var searchModel = new SearchModel(), _resultsView; diff --git a/src/features/QuickViewManager.js b/src/features/QuickViewManager.js index 53ccf5b08a..17a47f7adf 100644 --- a/src/features/QuickViewManager.js +++ b/src/features/QuickViewManager.js @@ -794,4 +794,5 @@ define(function (require, exports, module) { exports.isQuickViewShown = isQuickViewShown; exports.lockQuickView = lockQuickView; exports.unlockQuickView = unlockQuickView; + exports.hideQuickView = hidePreview; }); diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 151440ece9..0cc3db380a 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -365,7 +365,7 @@ define(function (require, exports, module) { * @param {boolean} aborted - true if any provider returned a result with the 'aborted' flag set * @param fileName */ - function updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted, fileName) { + function updatePanelTitleAndStatusBar(numProblems, numInfos, providersReportingProblems, aborted, fileName) { $fixAllBtn.addClass("forced-hidden"); var message, tooltip; @@ -405,6 +405,12 @@ define(function (require, exports, module) { return; } + // Info-level diagnostics (e.g. LSP suggestions) are shown as rows but aren't "problems"; + // surface their count in the title so "0 Problems" never sits above a visible info row. + if (numInfos > 0) { + message = StringUtils.format(Strings.ERRORS_PANEL_TITLE_INFO_SUFFIX, message, numInfos); + } + $problemsPanel.find(".title").text(message); tooltip = StringUtils.format(Strings.STATUSBAR_CODE_INSPECTION_TOOLTIP, message); let iconType = "inspection-errors"; @@ -762,6 +768,7 @@ define(function (require, exports, module) { if (providerList && providerList.length) { let numProblems = 0, + numInfos = 0, aborted = false, allErrors = [], html, @@ -825,7 +832,9 @@ define(function (require, exports, module) { error.codeSnippet = error.codeSnippet.substr(0, 175); // limit snippet width } - if (error.type !== Type.META) { + if (error.type === Type.META) { + numInfos++; + } else { numProblems++; } @@ -860,7 +869,7 @@ define(function (require, exports, module) { .empty() .append(html); // otherwise scroll pos from previous contents is remembered - updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted, + updatePanelTitleAndStatusBar(numProblems, numInfos, providersReportingProblems, aborted, path.basename(fullFilePath)); setGotoEnabled(true); diff --git a/src/languageTools/BracketsToNodeInterface.js b/src/languageTools/BracketsToNodeInterface.js deleted file mode 100644 index fe90a01c19..0000000000 --- a/src/languageTools/BracketsToNodeInterface.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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) { - - - 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 deleted file mode 100644 index e9686970ca..0000000000 --- a/src/languageTools/ClientLoader.js +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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) { - - - var ToolingInfo = JSON.parse(require("text!languageTools/ToolingInfo.json")), - NodeDomain = require("utils/NodeDomain"), - FileUtils = require("file/FileUtils"), - EventDispatcher = require("utils/EventDispatcher"), - BracketsToNodeInterface = require("languageTools/BracketsToNodeInterface").BracketsToNodeInterface; - - EventDispatcher.makeEventDispatcher(exports); - //Register paths required for Language Client and also register default brackets capabilities. - var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(); - // The native directory path ends with either "test" or "src". - _bracketsPath = _bracketsPath.replace(/\/test$/, "/src"); // convert from "test" to "src" - - var _modulePath = FileUtils.getNativeModuleDirectoryPath(module), - _nodePath = "node/RegisterLanguageClientInfo", - _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), - clientInfoDomain = null, - clientInfoLoadedPromise = null, - //Clients that have to be loaded once the LanguageClient info is successfully loaded on the - //node side. - pendingClientsToBeLoaded = []; - - function syncPrefsWithDomain(languageToolsPrefs) { - if (clientInfoDomain) { - clientInfoDomain.exec("syncPreferences", languageToolsPrefs); - } - } - - function _createNodeDomain(domainName, domainPath) { - return new NodeDomain(domainName, domainPath); - } - - function loadLanguageClientDomain(clientName, domainPath) { - //generate a random hash name for the domain, this is the client id - var domainName = clientName, - result = $.Deferred(), - languageClientDomain = _createNodeDomain(domainName, domainPath); - - if (languageClientDomain) { - languageClientDomain.promise() - .done(function () { - console.log(domainPath + " domain successfully created"); - result.resolve(languageClientDomain); - }) - .fail(function (err) { - console.error(domainPath + " domain could not be created."); - result.reject(); - }); - } else { - console.error(domainPath + " domain could not be created."); - result.reject(); - } - - return result; - } - - function createNodeInterfaceForDomain(languageClientDomain) { - var nodeInterface = new BracketsToNodeInterface(languageClientDomain); - - return nodeInterface; - } - - function _clientLoader(clientName, clientFilePath, clientPromise) { - loadLanguageClientDomain(clientName, clientFilePath) - .then(function (languageClientDomain) { - var languageClientInterface = createNodeInterfaceForDomain(languageClientDomain); - - clientPromise.resolve({ - name: clientName, - interface: languageClientInterface - }); - }, clientPromise.reject); - } - - function initiateLanguageClient(clientName, clientFilePath) { - var result = $.Deferred(); - - //Only load clients after the LanguageClient Info has been initialized - if (!clientInfoLoadedPromise || clientInfoLoadedPromise.state() === "pending") { - var pendingClient = { - load: _clientLoader.bind(null, clientName, clientFilePath, result) - }; - pendingClientsToBeLoaded.push(pendingClient); - } else { - _clientLoader(clientName, clientFilePath, result); - } - - return result; - } - - /** - * This function passes Brackets's native directory path as well as the tooling commands - * required by the LanguageClient node module. This information is then maintained in memory - * in the node process server for succesfully loading and functioning of all language clients - * since it is a direct dependency. - */ - function sendLanguageClientInfo() { - //Init node with Information required by Language Client - clientInfoLoadedPromise = clientInfoDomain.exec("initialize", _bracketsPath, ToolingInfo); - - function logInitializationError() { - console.error("Failed to Initialize LanguageClient Module Information."); - } - - //Attach success and failure function for the clientInfoLoadedPromise - clientInfoLoadedPromise.then(function (success) { - if (!success) { - logInitializationError(); - return; - } - - if (Array.isArray(pendingClientsToBeLoaded)) { - pendingClientsToBeLoaded.forEach(function (pendingClient) { - pendingClient.load(); - }); - } else { - exports.trigger("languageClientModuleInitialized"); - } - pendingClientsToBeLoaded = null; - }, function () { - logInitializationError(); - }); - } - - /** - * This function starts a domain which initializes the LanguageClient node module - * required by the Language Server Protocol framework in Brackets. All the LSP clients - * can only be successfully initiated once this domain has been successfully loaded and - * the LanguageClient info initialized. Refer to sendLanguageClientInfo for more. - */ - function initDomainAndHandleNodeCrash() { - clientInfoDomain = new NodeDomain("LanguageClientInfo", _domainPath); - //Initialize LanguageClientInfo once the domain has successfully loaded. - clientInfoDomain.promise().done(function () { - sendLanguageClientInfo(); - //This is to handle the node failure. If the node process dies, we get an on close - //event on the websocket connection object. Brackets then spawns another process and - //restablishes the connection. Once the connection is restablished we send reinitialize - //the LanguageClient info. - clientInfoDomain.connection.on("close", function (event, reconnectedPromise) { - reconnectedPromise.done(sendLanguageClientInfo); - }); - }).fail(function (err) { - console.error("ClientInfo domain could not be loaded: ", err); - }); - } - //initDomainAndHandleNodeCrash(); - - - exports.initiateLanguageClient = initiateLanguageClient; - exports.syncPrefsWithDomain = syncPrefsWithDomain; -}); diff --git a/src/languageTools/DefaultEventHandlers.js b/src/languageTools/DefaultEventHandlers.js deleted file mode 100644 index b8ae117484..0000000000 --- a/src/languageTools/DefaultEventHandlers.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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) { - - - 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 index 816c45b719..b5434d475f 100644 --- a/src/languageTools/DefaultProviders.js +++ b/src/languageTools/DefaultProviders.js @@ -31,18 +31,106 @@ define(function (require, exports, module) { var EditorManager = require('editor/EditorManager'), DocumentManager = require('document/DocumentManager'), - ExtensionUtils = require("utils/ExtensionUtils"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), - TokenUtils = require("utils/TokenUtils"), StringMatch = require("utils/StringMatch"), CodeInspection = require("language/CodeInspection"), PathConverters = require("languageTools/PathConverters"), + TabstopManager = require("editor/TabstopManager"), + marked = require("thirdparty/marked.min"), matcher = new StringMatch.StringMatcher({ preferPrefixMatches: true }); - ExtensionUtils.loadStyleSheet(module, "styles/default_provider_style.css"); + // Provider styles live in src/styles/brackets.less (core stylesheet) now that languageTools is a + // core module - no per-extension stylesheet to load. + + // ----- LSP code-hint documentation popup --------------------------------------------------- + // The signature (type) is shown inline in the hint list; the (potentially long) documentation + // is shown in a separate popup beside the hint list so the list itself never reflows while + // navigating with the arrow keys. + var $lspDocPopup = null; + + function _hideDocPopup() { + if ($lspDocPopup) { + $lspDocPopup.hide().empty(); + } + } + + function _docToHtml(documentation) { + if (!documentation) { + return ""; + } + var md = (typeof documentation === "string") ? documentation : (documentation.value || ""); + if (!md.trim()) { + return ""; + } + try { + return marked.parse(md); + } catch (e) { + return _.escape(md); + } + } + + function _showDocPopup($hint, docHtml) { + var $menu = $hint.closest(".codehint-menu"); + if (!docHtml || !$menu.length) { + _hideDocPopup(); + return; + } + // Make the popup a child of the hint menu so it is removed automatically when the menu is + // removed (CodeHintList.close() always does $hintMenu.remove()). This ties its lifecycle + // strictly to the code-hint list, regardless of which teardown path fires. It uses + // position:fixed, so it is still placed in viewport coordinates and is never clipped. + if (!$lspDocPopup || !$lspDocPopup.parent().is($menu)) { + $lspDocPopup = $("
").addClass("lsp-hint-doc-popup").appendTo($menu); + } + $lspDocPopup.empty().html(docHtml); + + // Anchor to the actual visible list (ul.dropdown-menu), not the zero-width .codehint-menu + // positioning element, so the popup sits flush beside the list without overlapping it. + var $list = $hint.closest("ul.dropdown-menu"); + if (!$list.length) { + $list = $menu; + } + var anchor = $list[0].getBoundingClientRect(); + + var GAP = 6; + // Measure, then place to the right of the hint list - flipping to the left when there + // isn't enough room. + $lspDocPopup.css({ display: "block", visibility: "hidden", left: 0, top: 0 }); + var winW = $(window).width(), winH = $(window).height(), + pw = $lspDocPopup.outerWidth(), ph = $lspDocPopup.outerHeight(), + left = anchor.right + GAP; + if (left + pw > winW - 8) { + left = anchor.left - pw - GAP; // not enough room on the right - flip to the left + } + left = Math.max(8, left); + var top = Math.min(anchor.top, Math.max(8, winH - ph - 8)); + $lspDocPopup.css({ left: left, top: top, visibility: "visible" }); + } + + function _injectInlineSignature($labelSpan, detail) { + if (!detail || !detail.trim() || detail.trim() === "?") { + return; + } + // Append the signature as a sibling of the label (inside .codehint-item) so the flex row + // layout (see CSS) lays out label + signature side-by-side without overlap or width change. + var text = detail.split("->").join(":").replace(/\s+/g, " ").trim(); + var $container = $labelSpan.parent(); // .codehint-item + if (!$container.length) { + $container = $labelSpan; + } + $container.find(".lsp-hint-sig").remove(); + // title shows the full signature on hover when it's truncated. + $("").addClass("lsp-hint-sig").text(text).attr("title", text).appendTo($container); + } + + // Characters that, when typed, implicitly trigger completion in the generic default policy + // (identifiers across most languages). A language server config may fully replace this policy + // with its own `shouldAutoTrigger(implicitChar, editor)` callback. Server-declared + // `triggerCharacters` (e.g. ".", "$", "::", "->") are honored per-language in the default too. + var DEFAULT_IDENTIFIER_CHARS = /[A-Za-z0-9_$]/; function setClient(client) { if (client) { @@ -58,26 +146,6 @@ define(function (require, exports, module) { CodeHintsProvider.prototype.setClient = setClient; - function formatTypeDataForToken($hintObj, token) { - $hintObj.addClass('brackets-hints-with-type-details'); - if (token.detail) { - if (token.detail.trim() !== '?') { - if (token.detail.length < 30) { - $('' + token.detail.split('->').join(':').toString().trim() + '').appendTo($hintObj).addClass("brackets-hints-type-details"); - } - $('' + token.detail.split('->').join(':').toString().trim() + '').appendTo($hintObj).addClass("hint-description"); - } - } else { - if (token.keyword) { - $('keyword').appendTo($hintObj).addClass("brackets-hints-keyword"); - } - } - if (token.documentation) { - $hintObj.attr('title', token.documentation); - $('').text(token.documentation.trim()).appendTo($hintObj).addClass("hint-doc"); - } - } - function filterWithQueryAndMatcher(hints, query) { var matchResults = $.map(hints, function (hint) { var searchResult = matcher.match(hint.label, query); @@ -103,7 +171,33 @@ define(function (require, exports, module) { return false; } - return true; + // Explicit invocation (e.g. Ctrl-Space) always shows hints. + if (!implicitChar) { + return true; + } + + // A language server may fully control where hints implicitly appear by supplying a + // `shouldAutoTrigger(implicitChar, editor)` callback (e.g. to suppress inside strings or + // trigger on language-specific sequences). When provided, it replaces the default policy. + var config = this.client.config || {}; + if (typeof config.shouldAutoTrigger === "function") { + return !!config.shouldAutoTrigger(implicitChar, editor); + } + + // Default policy: auto-trigger only on identifier characters, so the popup doesn't appear + // aggressively on every keystroke (operators, punctuation, etc.). + if (DEFAULT_IDENTIFIER_CHARS.test(implicitChar)) { + return true; + } + + // ...or on a server-declared trigger character (e.g. "." for member access). Whitespace + // triggers (some servers list " ") are intentionally ignored - they are too aggressive. + var triggerChars = serverCapabilities.completionProvider.triggerCharacters || []; + if (implicitChar.trim() && triggerChars.indexOf(implicitChar) !== -1) { + return true; + } + + return false; }; CodeHintsProvider.prototype.getHints = function (implicitChar) { @@ -121,10 +215,17 @@ define(function (require, exports, module) { 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); + var hints = []; + + // The query is the identifier prefix already typed before the cursor (empty right + // after a trigger char such as "."). Deriving it from the raw token is wrong - after a + // "." the token is "." itself, which would filter out every member completion. + var lineText = editor.document.getLine(pos.line), + queryStart = pos.ch; + while (queryStart > 0 && /[\w$]/.test(lineText.charAt(queryStart - 1))) { + queryStart--; + } + self.query = lineText.substring(queryStart, pos.ch); if (msgObj) { var res = msgObj.items, filteredHints = filterWithQueryAndMatcher(res, self.query); @@ -149,13 +250,15 @@ define(function (require, exports, module) { } $fHint.data("token", element); - formatTypeDataForToken($fHint, element); + // The signature is added inline lazily on highlight (onHighlight); the + // documentation is shown in a side popup. See _injectInlineSignature. hints.push($fHint); }); } $deferredHints.resolve({ - "hints": hints + "hints": hints, + "selectInitial": true }); }).fail(function () { $deferredHints.reject(); @@ -164,39 +267,119 @@ define(function (require, exports, module) { return $deferredHints; }; - CodeHintsProvider.prototype.insertHint = function ($hint) { - var editor = EditorManager.getActiveEditor(), - cursor = editor.getCursorPos(), - token = $hint.data("token"), - txt = null, - query = this.query, - shouldIgnoreQuery = this.ignoreQuery.includes(query), - inclusion = shouldIgnoreQuery ? "" : query, - start = { - line: cursor.line, - ch: cursor.ch - inclusion.length - }, - end = { - line: cursor.line, - ch: cursor.ch - }; + /** + * Called when a hint is highlighted. LSP completion lists are lightweight - the type detail + * and documentation usually arrive only via `completionItem/resolve`. Resolve the highlighted + * item lazily and render its type/docs inline (reusing formatTypeDataForToken so the look + * matches the rest of the hint UI). + */ + CodeHintsProvider.prototype.onHighlight = function ($hint) { + var self = this; + if (!self.client) { + _hideDocPopup(); + return; + } + var $span = $hint.closest("li").data("hint"), + token = $span && $span.data && $span.data("token"); + if (!token) { + _hideDocPopup(); + return; + } - 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 - }; + // Mark this menu as an LSP menu so the flex row layout + stable min-width apply (scoped so + // other code-hint providers are unaffected). Done synchronously so it takes effect before + // the list is shown. + $hint.closest(".codehint-menu").addClass("lsp-hints"); + + function present() { + // Inline: the signature for the highlighted row. Re-inject every time (it is + // idempotent) because the list DOM is rebuilt on each keystroke, which drops a + // previously-injected signature. resolveCompletion is cached separately (_lspResolved), + // so this does not cause extra LSP requests. + if (token.detail) { + _injectInlineSignature($span, token.detail); + } + // Beside the list: the (possibly long) documentation. + _showDocPopup($hint, _docToHtml(token.documentation)); } - if (editor) { - editor.document.replaceRange(txt, start, end); + if (token._lspResolved || !self.client.resolveCompletion) { + present(); + return; } + self.client.resolveCompletion(token).done(function (resolved) { + token._lspResolved = true; + token.detail = resolved.detail || token.detail; + token.documentation = resolved.documentation || token.documentation; + // Auto-import edits are commonly supplied only by completionItem/resolve. Carry them (and + // a resolve-provided textEdit, when the item lacked one) onto the token so insertHint can + // apply them. + if (resolved.additionalTextEdits) { + token.additionalTextEdits = resolved.additionalTextEdits; + } + if (!token.textEdit && resolved.textEdit) { + token.textEdit = resolved.textEdit; + } + present(); + }); + }; + + CodeHintsProvider.prototype.onClose = function () { + _hideDocPopup(); + }; + + CodeHintsProvider.prototype.insertHint = function ($hint) { + var editor = EditorManager.getActiveEditor(); + if (!editor) { + return false; + } + var token = $hint.data("token") || {}, + cursor = editor.getCursorPos(), + lineText = editor.document.getLine(cursor.line), + textEditRange = token.textEdit && token.textEdit.range, + startCh, + endCh = cursor.ch; + // Anchor the replacement start on the server-provided textEdit.range.start when it is usable. + // That start is stable as the user types forward (word/member starts don't move) and, crucially, + // for member completions it points AT the trigger "." while newText itself includes the dot + // (e.g. "console." + item ".log" -> replace from the "." -> "console.log", not "console..log"). + // We deliberately end at the CURRENT cursor rather than token.textEdit.range.end: that end is + // stale when completions are served from cache while typing continues, which would otherwise + // replace only part of the word (e.g. "conso"+enter -> "consolenso"). + if (textEditRange && textEditRange.start.line === cursor.line && + textEditRange.start.character <= cursor.ch) { + startCh = textEditRange.start.character; + } else { + startCh = cursor.ch; + while (startCh > 0 && /[\w$]/.test(lineText.charAt(startCh - 1))) { + startCh--; + } + } + var rawText = (token.textEdit && token.textEdit.newText) || token.insertText || token.label || "", + startPos = { line: cursor.line, ch: startCh }, + endPos = { line: cursor.line, ch: endCh }; + + if (token.insertTextFormat === 2) { + // Snippet completion: let TabstopManager expand $1 / ${1:x} / $0, place the caret at the + // first stop (selecting any default placeholder) and, for multi-stop snippets, start a + // Tab-navigable session. Its caret/markers follow the additionalTextEdits applied below. + TabstopManager.insertSnippet(editor, rawText, startPos, endPos); + } else { + editor.document.replaceRange(rawText, startPos, endPos); + } + + // Apply additionalTextEdits (e.g. the auto-import line). Bottom-to-top keeps their own + // coordinates valid; the editor maps the caret and any snippet markers through these edits + // automatically, so an import inserted above does not strand the cursor. + (token.additionalTextEdits || []).slice().sort(function (a, b) { + return (b.range.start.line - a.range.start.line) || + (b.range.start.character - a.range.start.character); + }).forEach(function (te) { + editor.document.replaceRange(te.newText, + { line: te.range.start.line, ch: te.range.start.character }, + { line: te.range.end.line, ch: te.range.end.character }); + }); + // Return false to indicate that another hinting session is not needed return false; }; @@ -301,13 +484,17 @@ define(function (require, exports, module) { /** * Method to handle jump to definition feature. */ - JumpToDefProvider.prototype.doJumpToDef = function () { - if (!this.client) { + JumpToDefProvider.prototype.doJumpToDef = function (editor) { + // JumpToDefManager passes the active editor; prefer it. Fall back to the focused/active + // editor only if called without one. getFocusedEditor() alone is unreliable - it returns + // null whenever the editor does not currently hold DOM focus (e.g. jump invoked from a + // menu/command, or in tests), which previously crashed here on a null editor. + editor = editor || EditorManager.getFocusedEditor() || EditorManager.getActiveEditor(); + if (!this.client || !editor) { return null; } - var editor = EditorManager.getFocusedEditor(), - pos = editor.getCursorPos(), + const pos = editor.getCursorPos(), docPath = editor.document.file._path, docPathUri = PathConverters.pathToUri(docPath), $deferredHints = $.Deferred(); @@ -351,6 +538,7 @@ define(function (require, exports, module) { function LintingProvider() { this._results = new Map(); this._promiseMap = new Map(); + this._lastSignature = new Map(); this._validateOnType = false; } @@ -362,10 +550,12 @@ define(function (require, exports, module) { if (filePathProvided) { this._results.delete(filePath); this._promiseMap.delete(filePath); + this._lastSignature.delete(filePath); } else { //clear all results this._results.clear(); this._promiseMap.clear(); + this._lastSignature.clear(); } }; @@ -396,15 +586,49 @@ define(function (require, exports, module) { this._promiseMap.get(filePath).resolve(this._results.get(filePath)); this._promiseMap.delete(filePath); } - if (this._validateOnType) { + // Language servers re-publish diagnostics in waves (e.g. syntax then semantic passes) and + // again on every edit - frequently with identical content. Re-running inspection only to + // render the same problems rebuilds the Problems panel for nothing, which both wastes work + // and detaches live DOM (e.g. the inline "fix" buttons a user may be clicking). Skip the + // re-run when this file's diagnostics are unchanged from what we last surfaced. A file the + // server has nothing to say about is first published as an EMPTY set, so treat "never + // recorded" as already-empty - that initial empty publish must NOT count as a change, or it + // would needlessly rebuild the panel (this was detaching fix buttons mid-click in tests). + var signature = JSON.stringify(errors), + previous = this._lastSignature.has(filePath) ? this._lastSignature.get(filePath) : "[]", + changed = previous !== signature; + this._lastSignature.set(filePath, signature); + if (this._validateOnType && changed) { var editor = EditorManager.getActiveEditor(), docPath = editor ? editor.document.file._path : ""; - if (filePath === docPath) { + // Only nudge CodeInspection to re-pull when this LSP inspector is actually a + // registered provider for the active file. In the app it always is, so behaviour + // is unchanged. But tests that take manual control of the inspection pipeline call + // CodeInspection._unregisterAll() and choreograph their own (mock) linters; a stray + // requestRun() fired by the live server's async diagnostics would restart those + // carefully-timed runs and flake the results. + if (filePath === docPath && this._isRegisteredInspector(docPath)) { CodeInspection.requestRun(); } } }; + /** + * @private + * @param {string} filePath - active document path + * @return {boolean} true if this provider's CodeInspection registration is still active for + * the file (or if no registration name was recorded, preserving legacy behaviour). + */ + LintingProvider.prototype._isRegisteredInspector = function (filePath) { + if (!this._inspectionProviderName) { + return true; + } + var name = this._inspectionProviderName; + return CodeInspection.getProvidersForPath(filePath).some(function (provider) { + return provider.name === name; + }); + }; + LintingProvider.prototype.getInspectionResultsAsync = function (fileText, filePath) { var result = $.Deferred(); diff --git a/src/languageTools/DocumentHighlight.js b/src/languageTools/DocumentHighlight.js new file mode 100644 index 0000000000..e60f8ad2ba --- /dev/null +++ b/src/languageTools/DocumentHighlight.js @@ -0,0 +1,177 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * DocumentHighlight - highlights every occurrence of the symbol under the cursor in the active + * editor, via the LSP `textDocument/documentHighlight` request. This is the generic, multi-language + * replacement for the Tern-only "highlight references under cursor" feature + * (extensions/default/JavaScriptRefactoring/HighLightReferences.js). + * + * It works for any registered language server that advertises `documentHighlightProvider`. Marks + * reuse the same visual style as the legacy feature (Editor.getMarkOptionMatchingRefs()). + * + * @module languageTools/DocumentHighlight + */ +define(function (require, exports, module) { + + + const EditorManager = require("editor/EditorManager"), + Editor = require("editor/Editor").Editor; + + const HIGHLIGHT_MARKER = "lsp-document-highlight"; + const REQUEST_DEBOUNCE_MS = 150; + + // Token types we never highlight on (comments/strings/numbers aren't symbols). + const SKIP_TOKEN_TYPES = ["comment", "string", "number"]; + + let initialized = false; + const registeredClients = []; // LanguageClient[] + let lastTokenKey = null; + let debounceTimer = null; + let requestSeq = 0; + + function _clientForEditor(editor) { + if (!editor) { + return null; + } + const langId = editor.document.getLanguage().getId(); + for (let i = 0; i < registeredClients.length; i++) { + const c = registeredClients[i]; + if (c.capabilities && c.capabilities.documentHighlightProvider && + c.languages.indexOf(langId) !== -1) { + return c; + } + } + return null; + } + + function _hasSingleCursor(editor) { + const selections = editor.getSelections(); + if (selections.length > 1) { + return false; // multi-cursor: don't highlight + } + const start = selections[0].start, end = selections[0].end; + return start.line === end.line && start.ch === end.ch; // a caret, not a range + } + + function _cursorActivity(_evt, editor) { + const client = _clientForEditor(editor); + if (!client) { + return; + } + if (!_hasSingleCursor(editor)) { + editor.clearAllMarks(HIGHLIGHT_MARKER); + lastTokenKey = null; + return; + } + + const pos = editor.getCursorPos(); + const token = editor.getToken(pos); + const tokenKey = pos.line + ":" + (token ? token.start + ":" + token.string : pos.ch); + if (tokenKey === lastTokenKey) { + return; // still on the same token - keep existing marks + } + + editor.clearAllMarks(HIGHLIGHT_MARKER); + lastTokenKey = tokenKey; + + if (!token || !token.string || !/[\w$]/.test(token.string) || + (token.type && SKIP_TOKEN_TYPES.indexOf(token.type) !== -1)) { + return; + } + + if (debounceTimer) { + clearTimeout(debounceTimer); + } + const seq = ++requestSeq; + debounceTimer = setTimeout(function () { + debounceTimer = null; + client.documentHighlight({ filePath: editor.document.file._path, cursorPos: pos }) + .done(function (highlights) { + // Ignore stale responses (cursor moved / another request issued / editor changed). + if (seq !== requestSeq || EditorManager.getActiveEditor() !== editor) { + return; + } + if (!highlights || !highlights.length) { + return; + } + editor.operation(function () { + highlights.forEach(function (h) { + if (!h || !h.range) { + return; + } + editor.markText(HIGHLIGHT_MARKER, + { line: h.range.start.line, ch: h.range.start.character }, + { line: h.range.end.line, ch: h.range.end.character }, + Editor.getMarkOptionMatchingRefs()); + }); + }); + }) + .fail(function () { /* no highlights for this position */ }); + }, REQUEST_DEBOUNCE_MS); + } + + function _activeEditorChanged(evt, current, previous) { + if (previous) { + previous.off("cursorActivity.lspHighlight"); + } + if (current) { + current.off("cursorActivity.lspHighlight"); + current.on("cursorActivity.lspHighlight", _cursorActivity); + lastTokenKey = null; + _cursorActivity(evt, current); + } + } + + /** + * Attach the active-editor / cursor listeners. Safe to call multiple times. + */ + function init() { + if (initialized) { + return; + } + initialized = true; + EditorManager.on("activeEditorChange", _activeEditorChanged); + const editor = EditorManager.getActiveEditor(); + if (editor) { + _activeEditorChanged(null, editor, null); + } + } + + /** + * Register a client so its languages get cursor-based occurrence highlighting. + * @param {Object} client - a LanguageClient + */ + function registerClient(client) { + if (registeredClients.indexOf(client) === -1) { + registeredClients.push(client); + } + // Re-evaluate the current editor now that a new server is available. + const editor = EditorManager.getActiveEditor(); + if (editor) { + lastTokenKey = null; + _cursorActivity(null, editor); + } + } + + exports.init = init; + exports.registerClient = registerClient; + exports.HIGHLIGHT_MARKER = HIGHLIGHT_MARKER; +}); diff --git a/src/languageTools/DocumentSync.js b/src/languageTools/DocumentSync.js new file mode 100644 index 0000000000..ae400716ea --- /dev/null +++ b/src/languageTools/DocumentSync.js @@ -0,0 +1,272 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * DocumentSync - drives LSP textDocument lifecycle (didOpen / didChange / didClose) from + * Phoenix document events. + * + * Open/close are ref-counted for free: Phoenix creates a `Document` on first reference and + * disposes it (firing `beforeDocumentDelete`) only when the last editor/working-set reference + * is gone, so a file open in multiple panes is opened once and closed once. + * + * `didChange` is debounced and sends the full document text (simple and correct; incremental + * sync can be added later). Before any feature request, `flush()` sends the latest pending + * change synchronously so the server's view matches the cursor. + * + * @module languageTools/DocumentSync + */ +define(function (require, exports, module) { + + + const DocumentManager = require("document/DocumentManager"), + EditorManager = require("editor/EditorManager"); + + const CHANGE_DEBOUNCE_MS = 400; + + let initialized = false; + const registeredClients = []; // LanguageClient[] + const tracked = new Map(); // vfsPath -> { client, version, open, pendingTimer } + + function _isSupported(client, doc) { + if (!client || !client.capabilities || !doc) { + return false; + } + if (doc.isUntitled && doc.isUntitled()) { + return false; + } + return client.languages.indexOf(doc.getLanguage().getId()) !== -1; + } + + function _clientForDoc(doc) { + for (let i = 0; i < registeredClients.length; i++) { + if (_isSupported(registeredClients[i], doc)) { + return registeredClients[i]; + } + } + return null; + } + + function _lspLanguageId(client, doc) { + const langId = doc.getLanguage().getId(); + const map = client.config && client.config.languageIdMap; + return (map && map[langId]) || langId; + } + + function _open(client, doc) { + const vfsPath = doc.file.fullPath; + const text = doc.getText(); + const state = { client: client, version: 1, open: true, pendingTimer: null, lastSentText: text }; + tracked.set(vfsPath, state); + client.notifyDidOpen(client.uriForPath(vfsPath), _lspLanguageId(client, doc), state.version, text); + } + + function _change(client, doc) { + const vfsPath = doc.file.fullPath; + const state = tracked.get(vfsPath); + if (!state || !state.open) { + _open(client, doc); + return; + } + const text = doc.getText(); + state.version += 1; + state.lastSentText = text; + client.notifyDidChange(client.uriForPath(vfsPath), state.version, text); + } + + function _close(doc) { + const vfsPath = doc.file.fullPath; + const state = tracked.get(vfsPath); + if (!state) { + return; + } + if (state.pendingTimer) { + clearTimeout(state.pendingTimer); + } + state.client.notifyDidClose(state.client.uriForPath(vfsPath)); + tracked.delete(vfsPath); + } + + function _onDocumentChange(event, doc) { + const client = _clientForDoc(doc); + if (!client) { + return; + } + const state = tracked.get(doc.file.fullPath); + if (!state || !state.open) { + _open(client, doc); + return; + } + if (state.pendingTimer) { + clearTimeout(state.pendingTimer); + } + state.pendingTimer = setTimeout(function () { + state.pendingTimer = null; + _change(client, doc); + }, CHANGE_DEBOUNCE_MS); + } + + function _onAfterDocumentCreate(event, doc) { + const client = _clientForDoc(doc); + if (client && !tracked.has(doc.file.fullPath)) { + _open(client, doc); + } + } + + function _onBeforeDocumentDelete(event, doc) { + _close(doc); + } + + function _onDocumentRefreshed(event, doc) { + if (!tracked.has(doc.file.fullPath)) { + return; + } + // Treat a refresh-from-disk as close + reopen so the server resyncs from a version 1. + _close(doc); + const client = _clientForDoc(doc); + if (client) { + _open(client, doc); + } + } + + function _onActiveEditorChange(event, current) { + // Safety net: guarantee the document the user is actually looking at is synced, even if its + // afterDocumentCreate happened while the server was (re)starting (e.g. session-restored + // files on a project switch). + if (!current || !current.document) { + return; + } + const doc = current.document; + const client = _clientForDoc(doc); + const state = tracked.get(doc.file.fullPath); + if (client && (!state || !state.open)) { + _open(client, doc); + } + } + + /** + * Attach the document lifecycle listeners. Safe to call multiple times. + */ + function init() { + if (initialized) { + return; + } + initialized = true; + DocumentManager.on(DocumentManager.EVENT_DOCUMENT_CHANGE, _onDocumentChange); + DocumentManager.on(DocumentManager.EVENT_AFTER_DOCUMENT_CREATE, _onAfterDocumentCreate); + DocumentManager.on(DocumentManager.EVENT_BEFORE_DOCUMENT_DELETE, _onBeforeDocumentDelete); + DocumentManager.on(DocumentManager.EVENT_DOCUMENT_REFRESHED, _onDocumentRefreshed); + EditorManager.on("activeEditorChange", _onActiveEditorChange); + } + + /** + * Register a client so its languages participate in document sync. + * @param {Object} client - a LanguageClient + */ + function registerClient(client) { + if (registeredClients.indexOf(client) === -1) { + registeredClients.push(client); + } + } + + /** + * Send didOpen for any already-open documents that this client supports. Used when a server + * starts (or restarts) after documents are already open. + * @param {Object} client - a LanguageClient + */ + function openSupportedDocuments(client) { + const docs = DocumentManager.getAllOpenDocuments().slice(); + // Belt-and-suspenders: the file the user is actually looking at - e.g. a session-restored + // document at app start - may not yet appear in getAllOpenDocuments() at the moment the + // server finishes starting. Include the active editor's document explicitly so it is synced + // immediately, instead of only after the next file switch (which is what triggered the + // activeEditorChange safety net before). + const activeEditor = EditorManager.getActiveEditor(); + if (activeEditor && activeEditor.document && docs.indexOf(activeEditor.document) === -1) { + docs.push(activeEditor.document); + } + docs.forEach(function (doc) { + if (_isSupported(client, doc)) { + // Always (re)send didOpen with the current content. This is called right after a + // server (re)start, so any prior tracking is stale and must not be trusted - e.g. + // a didOpen that failed during a restart's down-window would otherwise leave the + // file marked "open" but absent from the new server. + _open(client, doc); + } + }); + } + + /** + * Ensure the server has the latest content for a file before a feature request: opens the + * document if needed and flushes any pending debounced change immediately. + * @param {Object} client - a LanguageClient + * @param {string} vfsPath - the document's VFS path + * @return {Promise} + */ + function flush(client, vfsPath) { + return new Promise(function (resolve) { + const doc = DocumentManager.getOpenDocumentForPath(vfsPath); + if (!doc) { + resolve(); + return; + } + const state = tracked.get(vfsPath); + if (!state || !state.open) { + _open(client, doc); + } else { + // Always clear any pending debounce and send if the document text differs from what + // the server last received. Gating on `pendingTimer` alone is racy: EVENT_DOCUMENT_CHANGE + // has multiple listeners and the feature request (e.g. completion on ".") can reach + // flush() before _onDocumentChange has set the timer, leaving the server a keystroke + // behind - so a member completion ("console.") would be answered against stale text + // ("console") and return globals. + if (state.pendingTimer) { + clearTimeout(state.pendingTimer); + state.pendingTimer = null; + } + if (doc.getText() !== state.lastSentText) { + _change(client, doc); + } + } + resolve(); + }); + } + + /** + * Forget all documents tracked for a client (used when its server stops/restarts). Does not + * send didClose since the server is going away. + * @param {Object} client - a LanguageClient + */ + function clearServer(client) { + tracked.forEach(function (state, vfsPath) { + if (state.client === client) { + if (state.pendingTimer) { + clearTimeout(state.pendingTimer); + } + tracked.delete(vfsPath); + } + }); + } + + exports.init = init; + exports.registerClient = registerClient; + exports.openSupportedDocuments = openSupportedDocuments; + exports.flush = flush; + exports.clearServer = clearServer; +}); diff --git a/src/languageTools/HoverProvider.js b/src/languageTools/HoverProvider.js new file mode 100644 index 0000000000..0622a5b160 --- /dev/null +++ b/src/languageTools/HoverProvider.js @@ -0,0 +1,205 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * HoverProvider - shows LSP `textDocument/hover` documentation on mouse hover via the + * QuickViewManager. This is the one provider the legacy DefaultProviders did not have. + * + * @module languageTools/HoverProvider + */ +define(function (require, exports, module) { + + + // Hover styling lives in src/styles/brackets.less (.lsp-hover-quickview) so it tracks the + // active light/dark theme. + const marked = require("thirdparty/marked.min"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + KeyBindingManager = require("command/KeyBindingManager"), + QuickViewManager = require("features/QuickViewManager"), + Strings = require("strings"); + + /** + * Convert LSP hover `contents` (MarkupContent | MarkedString | string | array of those) + * into a single markdown string. + */ + function _contentsToMarkdown(contents) { + if (!contents) { + return ""; + } + if (typeof contents === "string") { + return contents; + } + if (Array.isArray(contents)) { + return contents.map(_contentsToMarkdown).filter(Boolean).join("\n\n---\n\n"); + } + if (contents.kind) { + // MarkupContent { kind: 'markdown' | 'plaintext', value } + return contents.value || ""; + } + if (contents.language) { + // MarkedString { language, value } + return "```" + contents.language + "\n" + (contents.value || "") + "\n```"; + } + return contents.value || ""; + } + + // Syntax-highlight the fenced code blocks (the type/function signature) using the globally + // available highlight.js, so the signature reads like code instead of flat monospace. The app + // ships the github-dark hljs theme, so the signature block is rendered on a dark chip in both + // light and dark editor themes (see .lsp-hover-quickview pre in brackets.less). + function _highlightCode(html) { + const hljs = (typeof Phoenix !== "undefined") && Phoenix.libs && Phoenix.libs.hljs; + if (!hljs) { + return html; + } + const $wrap = $("
").html(html); + $wrap.find("pre > code").each(function () { + const $code = $(this); + const match = ($code.attr("class") || "").match(/language-([\w-]+)/); + const lang = match && match[1]; + if (lang && hljs.getLanguage(lang)) { + try { + $code.html(hljs.highlight($code.text(), { language: lang }).value).addClass("hljs"); + } catch (e) { + // leave the block unhighlighted on any hljs error + } + } + }); + return $wrap.html(); + } + + function _renderContents(contents) { + const markdown = _contentsToMarkdown(contents); + if (!markdown || !markdown.trim()) { + return null; + } + try { + return _highlightCode(marked.parse(markdown)); + } catch (e) { + return null; + } + } + + /** + * Build one clickable action row (icon + label + optional shortcut). Clicking places the cursor + * at the hovered position, dismisses the hover popup, then runs the command - so jump/find + * operate on the symbol the user hovered, not wherever the cursor happened to be. + */ + function _action(iconClass, label, commandId, editor, pos, alignRight) { + const $action = $("
").addClass("lsp-hover-action").attr("tabindex", "0"); + if (alignRight) { + $action.addClass("lsp-hover-action--end"); + } + $("").addClass(iconClass + " lsp-hover-action-icon").appendTo($action); + $("").addClass("lsp-hover-action-label").text(label).appendTo($action); + // The keyboard shortcut is surfaced as a tooltip (not inline) to keep the row compact. + const shortcut = KeyBindingManager.getKeyBindingsDisplay(commandId); + $action.attr("title", shortcut ? (label + " (" + shortcut + ")") : label); + function run(e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + QuickViewManager.hideQuickView(); + editor.setCursorPos(pos.line, pos.ch); + editor.focus(); + CommandManager.execute(commandId); + } + $action.on("click", run); + $action.on("keydown", function (e) { + if (e.keyCode === 13 || e.keyCode === 32) { // Enter / Space + run(e); + } + }); + return $action; + } + + /** + * Build the action footer (Go to Definition / Find Usages), gated on what the server supports. + * @return {?jQuery} the actions element, or null when no action is available. + */ + function _buildActions(client, editor, pos) { + const caps = client.getServerCapabilities() || {}; + const $actions = $("
").addClass("lsp-hover-actions"); + let count = 0; + if (caps.definitionProvider) { + $actions.append(_action("fa-solid fa-arrow-right", Strings.CMD_JUMPTO_DEFINITION, + Commands.NAVIGATE_JUMPTO_DEFINITION, editor, pos)); + count++; + } + if (caps.referencesProvider) { + $actions.append(_action("fa-solid fa-magnifying-glass", Strings.FIND_ALL_REFERENCES, + Commands.CMD_FIND_ALL_REFERENCES, editor, pos, true)); + count++; + } + return count ? $actions : null; + } + + /** + * @param {Object} client - a LanguageClient from LSPClient.js + */ + function HoverProvider(client) { + this.client = client; + this.QUICK_VIEW_NAME = "lsp.hover." + client.serverId; + } + + HoverProvider.prototype.getQuickView = function (editor, pos, token, line) { + const self = this; + return new Promise(function (resolve, reject) { + if (!self.client || !self.client.getServerCapabilities() || + !self.client.getServerCapabilities().hoverProvider) { + reject(); + return; + } + const filePath = editor.document.file._path; + self.client.requestHover({ filePath: filePath, cursorPos: pos }) + .done(function (hover) { + const html = hover && _renderContents(hover.contents); + if (!html) { + reject(); + return; + } + let start = { line: pos.line, ch: token.start }; + let end = { line: pos.line, ch: token.end }; + if (hover.range) { + start = { line: hover.range.start.line, ch: hover.range.start.character }; + end = { line: hover.range.end.line, ch: hover.range.end.character }; + } + const $content = $("
").addClass("lsp-hover-quickview"); + $("
").addClass("lsp-hover-doc").html(html).appendTo($content); + const $actions = _buildActions(self.client, editor, pos); + if ($actions) { + $content.append($actions); + } + resolve({ + start: start, + end: end, + content: $content + }); + }) + .fail(function () { + reject(); + }); + }); + }; + + exports.HoverProvider = HoverProvider; +}); diff --git a/src/languageTools/LSPClient.js b/src/languageTools/LSPClient.js new file mode 100644 index 0000000000..ba21118bfd --- /dev/null +++ b/src/languageTools/LSPClient.js @@ -0,0 +1,693 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * LSPClient - browser-side Language Server Protocol client (desktop only). + * + * This is the thin, modern replacement for the legacy `LanguageClientWrapper` + NodeDomain + * transport. It owns a single shared `ph-lsp` NodeConnector and a multi-server registry + * keyed by `serverId`, and talks to the Node-side `src-node/lsp-client.js`. + * + * Language extensions only call `registerLanguageServer(config)`; this module then: + * - lazily loads the node LSP module on demand (keeps boot fast), + * - spawns + `initialize`s the server, + * - instantiates the standard providers from `DefaultProviders` (completion, signatureHelp, + * definition, references, diagnostics) plus the `HoverProvider`, and registers each with + * its Phoenix manager (CodeHintManager, ParameterHintsManager, JumpToDefManager, + * FindReferencesManager, CodeInspection, QuickViewManager), + * - drives document lifecycle through `DocumentSync`. + * + * Each `LanguageClient` exposes exactly the method surface `DefaultProviders` expects + * (`getServerCapabilities`, `requestHints`, `requestParameterHints`, `gotoDefinition`, + * `findReferences`, plus the new `requestHover`). All translation between Phoenix + * `{line, ch}` / VFS paths and LSP `{line, character}` / `file://` URIs (including the + * `/tauri` virtual-path prefix used by the desktop build) happens here so the providers stay + * transport-agnostic. + * + * @module languageTools/LSPClient + */ + +/* eslint max-len: ["error", { "code": 120 }] */ +define(function (require, exports, module) { + + + const NodeConnector = require("NodeConnector"), + NodeUtils = require("utils/NodeUtils"), + ProjectManager = require("project/ProjectManager"), + DocumentManager = require("document/DocumentManager"), + FileUtils = require("file/FileUtils"), + PathConverters = require("languageTools/PathConverters"), + DefaultProviders = require("languageTools/DefaultProviders"), + HoverProvider = require("languageTools/HoverProvider"), + DocumentSync = require("languageTools/DocumentSync"), + DocumentHighlight = require("languageTools/DocumentHighlight"), + LanguageManager = require("language/LanguageManager"), + CodeHintManager = require("editor/CodeHintManager"), + ParameterHintsManager = require("features/ParameterHintsManager"), + JumpToDefManager = require("features/JumpToDefManager"), + FindReferencesManager = require("features/FindReferencesManager"), + QuickViewManager = require("features/QuickViewManager"), + CodeInspection = require("language/CodeInspection"); + + const LSP_CONNECTOR_ID = "ph-lsp"; + // Relative path required on the node side (resolved from src-node/utils.js). Lazy-loads the + // node LSP module the first time we connect, so node boot is unaffected. + const NODE_LSP_MODULE = "./lsp-client"; + // LSP providers register above the built-in (e.g. Tern) providers so the language server + // wins when it is available, falling back gracefully when it is not. + const DEFAULT_PRIORITY = 1; + + let connectorPromise = null; + let connector = null; + const clients = new Map(); // serverId -> LanguageClient + + // ------------------------------------------------------------------------------------------ + // Path / coordinate translation (VFS <-> real OS path <-> file:// URI) + // ------------------------------------------------------------------------------------------ + + function _toPlatformPath(vfsPath) { + if (window.fs && window.fs.getTauriPlatformPath) { + const platformPath = window.fs.getTauriPlatformPath(vfsPath); + if (platformPath) { + return platformPath; + } + } + return vfsPath; + } + + function _toVirtualPath(platformPath) { + if (window.fs && window.fs.getTauriVirtualPath) { + return window.fs.getTauriVirtualPath(platformPath); + } + return platformPath; + } + + /** Convert a Phoenix VFS path to the `file://` URI the server understands (real OS path). */ + function pathToServerUri(vfsPath) { + return PathConverters.pathToUri(_toPlatformPath(vfsPath)); + } + + /** Convert a server `file://` URI (real OS path) back to a VFS-based `file://` URI. */ + function serverUriToVfsUri(serverUri) { + const platformPath = PathConverters.uriToPath(serverUri); + return PathConverters.pathToUri(_toVirtualPath(platformPath)); + } + + function _markupToString(documentation) { + if (!documentation) { + return ""; + } + if (typeof documentation === "string") { + return documentation; + } + return documentation.value || ""; + } + + function _paramLabel(signatureLabel, paramLabel) { + if (Array.isArray(paramLabel)) { + // LSP allows [start, end] offsets into the signature label. + return signatureLabel.substring(paramLabel[0], paramLabel[1]); + } + return paramLabel; + } + + function _normalizeLocation(loc) { + if (!loc) { + return null; + } + const uri = loc.uri || loc.targetUri; + const range = loc.range || loc.targetSelectionRange || loc.targetRange; + if (!uri || !range) { + return null; + } + return { uri: serverUriToVfsUri(uri), range: range }; + } + + // ------------------------------------------------------------------------------------------ + // Shared connector (lazy) + // ------------------------------------------------------------------------------------------ + + function getConnector() { + if (!connectorPromise) { + connectorPromise = (async function () { + // Lazy-load the node LSP module on first use so it does not slow node boot. + await NodeUtils._loadNodeExtensionModule(NODE_LSP_MODULE); + connector = NodeConnector.createNodeConnector(LSP_CONNECTOR_ID, {}); + connector.on("lspNotification", _onLspNotification); + connector.on("serverExit", _onServerExit); + connector.on("serverError", _onServerError); + return connector; + }()); + } + return connectorPromise; + } + + function _onLspNotification(_event, data) { + if (!data) { + return; + } + const client = clients.get(data.serverId); + if (!client) { + return; + } + if (data.method === "textDocument/publishDiagnostics" && client.lintingProvider) { + const params = data.params || {}; + // Rewrite the URI to a VFS-based URI so the linting provider keys results by the + // same path CodeInspection uses (editor.document.file._path). + const vfsUri = serverUriToVfsUri(params.uri); + let diagnostics = params.diagnostics || []; + // Let the language config drop diagnostics that don't make sense for a given file + // (e.g. TypeScript's "needs a declaration file" suggestions in a plain JS file). + const filterFn = client.config && client.config.filterDiagnostics; + if (filterFn && diagnostics.length) { + const vfsPath = PathConverters.uriToPath(vfsUri); + const language = LanguageManager.getLanguageForPath(vfsPath); + diagnostics = filterFn(diagnostics, { + languageId: language && language.getId(), + filePath: vfsPath + }); + } + client.lintingProvider.setInspectionResults({ + uri: vfsUri, + diagnostics: diagnostics + }); + } + } + + const MAX_AUTO_RESTARTS = 3; + + function _onServerExit(_event, data) { + const client = data && clients.get(data.serverId); + if (!client) { + return; + } + client.capabilities = null; + DocumentSync.clearServer(client); + if (client._stopping) { + return; // Intentional stop/restart - do not auto-restart here. + } + // Unexpected crash - log it loudly (with the server's stderr) so failures are never + // silent, then self-heal with a bounded backoff to recover without a reload. + console.error("[LSP] server '" + data.serverId + "' exited unexpectedly (code=" + data.code + + (data.signal ? ", signal=" + data.signal : "") + ")." + + (data.stderr ? "\n--- server stderr ---\n" + data.stderr : "")); + client._crashCount = (client._crashCount || 0) + 1; + if (client._crashCount > MAX_AUTO_RESTARTS) { + console.error("[LSP]", client.serverId, "exited repeatedly; not restarting"); + return; + } + setTimeout(function () { + if (!clients.has(client.serverId) || client.capabilities) { + return; + } + _startAndInit(client).then(function () { + client._crashCount = 0; + DocumentSync.openSupportedDocuments(client); + }).catch(function (err) { + console.error("[LSP] auto-restart failed", client.serverId, err && (err.message || err)); + }); + }, 1000 * client._crashCount); + } + + function _onServerError(_event, data) { + if (data) { + console.error("[LSP] server error", data.serverId, data.error); + } + } + + // ------------------------------------------------------------------------------------------ + // LanguageClient - one per server, exposes the provider-facing method surface + // ------------------------------------------------------------------------------------------ + + function LanguageClient(serverId, languages, config) { + this.serverId = serverId; + this.languages = languages; + this.config = config; + this.capabilities = null; + } + + LanguageClient.prototype.getServerCapabilities = function () { + return this.capabilities; + }; + + LanguageClient.prototype.uriForPath = function (vfsPath) { + return pathToServerUri(vfsPath); + }; + + LanguageClient.prototype._request = function (method, params) { + const serverId = this.serverId; + return getConnector().then(function (conn) { + return conn.execPeer("sendRequest", { serverId: serverId, method: method, params: params }); + }); + }; + + LanguageClient.prototype._notify = function (method, params) { + const serverId = this.serverId; + return getConnector().then(function (conn) { + return conn.execPeer("sendNotification", { serverId: serverId, method: method, params: params }); + }).catch(function (err) { + // Notifications are best-effort (the server may be restarting). Don't let it become an + // unhandled rejection, but still surface it as a warning so we are not blind. + console.warn("[LSP] notification '" + method + "' failed:", err && (err.message || err)); + }); + }; + + // Document lifecycle notifications used by DocumentSync. + LanguageClient.prototype.notifyDidOpen = function (uri, languageId, version, text) { + return this._notify("textDocument/didOpen", { + textDocument: { uri: uri, languageId: languageId, version: version, text: text } + }); + }; + LanguageClient.prototype.notifyDidChange = function (uri, version, text) { + return this._notify("textDocument/didChange", { + textDocument: { uri: uri, version: version }, + contentChanges: [{ text: text }] + }); + }; + LanguageClient.prototype.notifyDidClose = function (uri) { + return this._notify("textDocument/didClose", { textDocument: { uri: uri } }); + }; + + function _positionOf(cursorPos) { + return { line: cursorPos.line, character: cursorPos.ch }; + } + + // Build a cache key identifying the current completion "context": the file, line, the column + // where the word under the cursor starts, and the text on the line before that word. While the + // user types/moves within the same word, this key stays constant, so we can reuse the server's + // (complete) result and filter client-side instead of re-querying. That avoids slow late + // responses rebuilding the list mid-navigation. + function _completionContextKey(filePath, pos) { + const doc = DocumentManager.getOpenDocumentForPath(filePath); + if (!doc) { + return null; + } + const lineText = doc.getLine(pos.line) || ""; + let start = pos.ch; + while (start > 0 && /[\w$]/.test(lineText.charAt(start - 1))) { + start--; + } + return filePath + "|" + pos.line + "|" + lineText.substring(0, start); + } + + LanguageClient.prototype.requestHints = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + // Reuse the cached (complete) completion list while still in the same completion + // context, so typing/cursor-moves within a word don't re-hit the server. + const ctxKey = _completionContextKey(params.filePath, params.cursorPos); + if (ctxKey && self._completionCache && self._completionCache.key === ctxKey) { + deferred.resolve({ items: self._completionCache.items }); + return; + } + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/completion", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos) + }); + const isIncomplete = !!(result && !Array.isArray(result) && result.isIncomplete); + const items = (result && (result.items || result)) || []; + items.forEach(function (item) { + // Keep the full server item (its `data` is needed for completionItem/resolve); + // just coerce documentation to a string for inline display. + item.documentation = _markupToString(item.documentation); + }); + // Only cache a complete list (an incomplete one must be re-queried as the user types). + self._completionCache = (ctxKey && !isIncomplete) ? { key: ctxKey, items: items } : null; + deferred.resolve({ items: items }); + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + LanguageClient.prototype.requestParameterHints = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/signatureHelp", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos) + }); + if (!result || !result.signatures || !result.signatures.length) { + deferred.reject(); + return; + } + const signatures = result.signatures.map(function (sig) { + return { + documentation: _markupToString(sig.documentation) || sig.label, + parameters: (sig.parameters || []).map(function (p) { + return { + label: _paramLabel(sig.label, p.label), + documentation: _markupToString(p.documentation) + }; + }) + }; + }); + deferred.resolve({ signatures: signatures, activeParameter: result.activeParameter }); + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + LanguageClient.prototype.gotoDefinition = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/definition", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos) + }); + if (!result || (Array.isArray(result) && !result.length)) { + deferred.reject(); + return; + } + if (Array.isArray(result)) { + const locations = result.map(_normalizeLocation).filter(Boolean); + if (!locations.length) { + deferred.reject(); + return; + } + deferred.resolve(locations); + } else { + deferred.resolve(_normalizeLocation(result)); + } + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + LanguageClient.prototype.findReferences = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/references", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos), + context: { includeDeclaration: true } + }); + const locations = Array.isArray(result) ? result.map(_normalizeLocation).filter(Boolean) : []; + deferred.resolve(locations); + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + LanguageClient.prototype.resolveCompletion = function (item) { + const deferred = $.Deferred(); + if (!this.capabilities || !this.capabilities.completionProvider || + !this.capabilities.completionProvider.resolveProvider) { + return deferred.resolve(item).promise(); // server can't enrich items + } + this._request("completionItem/resolve", item).then(function (resolved) { + const out = resolved || item; + out.documentation = _markupToString(out.documentation); + deferred.resolve(out); + }, function () { + deferred.resolve(item); // fall back to the unresolved item + }); + return deferred.promise(); + }; + + LanguageClient.prototype.documentHighlight = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/documentHighlight", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos) + }); + deferred.resolve(Array.isArray(result) ? result : []); + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + LanguageClient.prototype.requestHover = function (params) { + const self = this; + const deferred = $.Deferred(); + (async function () { + try { + await DocumentSync.flush(self, params.filePath); + const result = await self._request("textDocument/hover", { + textDocument: { uri: self.uriForPath(params.filePath) }, + position: _positionOf(params.cursorPos) + }); + deferred.resolve(result); + } catch (err) { + console.warn("[LSP] request failed:", err && (err.message || err)); + deferred.reject(err); + } + }()); + return deferred.promise(); + }; + + // ------------------------------------------------------------------------------------------ + // Server lifecycle + provider registration + // ------------------------------------------------------------------------------------------ + + function _projectRootPath() { + const root = ProjectManager.getProjectRoot(); + return root ? root.fullPath : null; + } + + function _clientCapabilities() { + return { + textDocument: { + synchronization: { + dynamicRegistration: false, + didSave: true, + willSave: false, + willSaveWaitUntil: false + }, + completion: { + dynamicRegistration: false, + completionItem: { + snippetSupport: false, + documentationFormat: ["markdown", "plaintext"] + } + }, + hover: { dynamicRegistration: false, contentFormat: ["markdown", "plaintext"] }, + signatureHelp: { + dynamicRegistration: false, + signatureInformation: { documentationFormat: ["markdown", "plaintext"] } + }, + definition: { dynamicRegistration: false }, + references: { dynamicRegistration: false }, + publishDiagnostics: { relatedInformation: false } + }, + workspace: { workspaceFolders: true, configuration: false } + }; + } + + // The UI language the user has Phoenix set to (e.g. "en", "fr", "ja"), forwarded to the + // server so it can localize its messages. Falls back to English when unavailable. + function _uiLocale() { + return (typeof brackets !== "undefined" && brackets.getLocale && brackets.getLocale()) || "en"; + } + + async function _startAndInit(client) { + const config = client.config; + const conn = await getConnector(); + const rootVfsPath = (config.rootUriProvider && config.rootUriProvider()) || _projectRootPath(); + const rootUri = rootVfsPath ? pathToServerUri(rootVfsPath) : null; + const rootName = rootVfsPath ? FileUtils.getBaseName(rootVfsPath) : "root"; + + await conn.execPeer("startServer", { + serverId: client.serverId, + command: config.command, + args: config.args || ["--stdio"], + rootUri: rootUri + }); + + const initResult = await conn.execPeer("sendRequest", { + serverId: client.serverId, + method: "initialize", + params: { + processId: null, + // LSP InitializeParams.locale - the UI language to localize server messages + // (diagnostics, hover/quick-info) in. vtsls forwards this to tsserver, which ships + // localized messages for many locales and falls back to English for unknown ones. + locale: _uiLocale(), + rootUri: rootUri, + workspaceFolders: rootUri ? [{ uri: rootUri, name: rootName }] : null, + capabilities: _clientCapabilities(), + initializationOptions: config.initializationOptions || {} + } + }); + client.capabilities = (initResult && initResult.capabilities) || {}; + + await conn.execPeer("sendNotification", { + serverId: client.serverId, + method: "initialized", + params: {} + }); + } + + function _registerProviders(client) { + const langs = client.languages; + + client.codeHints = new DefaultProviders.CodeHintsProvider(client); + client.parameterHints = new DefaultProviders.ParameterHintsProvider(client); + client.jumpToDef = new DefaultProviders.JumpToDefProvider(client); + client.references = new DefaultProviders.ReferencesProvider(client); + client.lintingProvider = new DefaultProviders.LintingProvider(); + client.lintingProvider._validateOnType = true; + // recorded so the provider can tell whether it is still a participating inspector before nudging a + // re-run on async diagnostics. + client.lintingProvider._inspectionProviderName = client.serverId; + client.hover = new HoverProvider.HoverProvider(client); + + CodeHintManager.registerHintProvider(client.codeHints, langs, DEFAULT_PRIORITY); + ParameterHintsManager.registerHintProvider(client.parameterHints, langs, DEFAULT_PRIORITY); + JumpToDefManager.registerJumpToDefProvider(client.jumpToDef, langs, DEFAULT_PRIORITY); + FindReferencesManager.registerFindReferencesProvider(client.references, langs, DEFAULT_PRIORITY); + QuickViewManager.registerQuickViewProvider(client.hover, langs); + + langs.forEach(function (lang) { + CodeInspection.register(lang, { + name: client.lintingProvider._inspectionProviderName, + scanFileAsync: function (text, fullPath) { + // Diagnostics are pushed asynchronously by the server (publishDiagnostics), + // so never block the scan waiting for them - return whatever is cached now and + // let setInspectionResults() trigger a re-scan when fresh diagnostics arrive. + // (Blocking here would surface CodeInspection's 10s "timed out" error.) + const cached = client.lintingProvider.getInspectionResults(text, fullPath); + return $.Deferred().resolve(cached || { errors: [] }).promise(); + } + }); + }); + } + + /** + * Register and start a language server, wiring all providers into the editor. + * + * @param {Object} config + * @param {string} config.serverId - unique id for the server (e.g. "typescript") + * @param {string} config.command - server binary (resolved from node_modules/.bin then PATH) + * @param {string[]} [config.args=["--stdio"]] - server arguments + * @param {string[]} config.languages - Phoenix language ids this server handles + * @param {Object} [config.initializationOptions] - LSP initializationOptions for the server + * @param {Object} [config.languageIdMap] - map of Phoenix langId -> LSP languageId + * @param {function(string, Editor):boolean} [config.shouldAutoTrigger] - decides whether a + * typed character should implicitly open the hint list. Receives (implicitChar, editor). + * When omitted, a generic default is used (identifier chars + the server's non-whitespace + * triggerCharacters). Explicit invocation (Ctrl-Space) always shows hints regardless. + * @param {function():string} [config.rootUriProvider] - returns the workspace root VFS path + * @return {Promise} the client, or null if it could not be started + */ + async function registerLanguageServer(config) { + if (clients.has(config.serverId)) { + return clients.get(config.serverId); + } + const client = new LanguageClient(config.serverId, config.languages, config); + // Register eagerly so a publishDiagnostics arriving during init is not dropped. + clients.set(config.serverId, client); + try { + await _startAndInit(client); + _registerProviders(client); + DocumentSync.init(); + DocumentSync.registerClient(client); + DocumentSync.openSupportedDocuments(client); + DocumentHighlight.init(); + DocumentHighlight.registerClient(client); + return client; + } catch (err) { + console.error("[LSP] failed to start server", config.serverId, err && (err.message || err)); + clients.delete(config.serverId); + return null; + } + } + + /** + * Stop a running language server and restart it (e.g. on project switch) with the current + * workspace root. Provider registrations are preserved; only the server process is recycled. + * + * @param {string} serverId + * @return {Promise} + */ + async function restartLanguageServer(serverId) { + const client = clients.get(serverId); + if (!client) { + return; + } + await stopServerProcess(client); + try { + await _startAndInit(client); + DocumentSync.openSupportedDocuments(client); + // The find-references command's enabled state is computed on file switch; on a project + // switch that happens while the server is still restarting (capabilities not yet + // available), so it would be left disabled. Now that the server is back with its + // capabilities, refresh it for the active file so "Find Usages" works without requiring + // another file switch. + FindReferencesManager.setMenuItemStateForLanguage(); + } catch (err) { + console.error("[LSP] failed to restart server", serverId, err && (err.message || err)); + } + } + + async function stopServerProcess(client) { + const conn = await getConnector(); + client._stopping = true; // Suppress auto-restart for this intentional stop. + // Clear capabilities and document tracking up front so that, during the teardown + // down-window, no feature/sync request treats the server as alive and no failed didOpen + // leaves a stale "open" entry that would block the post-restart re-sync. + client.capabilities = null; + client._completionCache = null; + DocumentSync.clearServer(client); + try { + await conn.execPeer("sendRequest", { serverId: client.serverId, method: "shutdown", params: null }); + await conn.execPeer("sendNotification", { serverId: client.serverId, method: "exit", params: null }); + } catch (e) { + // Server may already be dead; fall through to a hard stop. + } + await conn.execPeer("stopServer", { serverId: client.serverId }); + client._stopping = false; + } + + exports.registerLanguageServer = registerLanguageServer; + exports.restartLanguageServer = restartLanguageServer; + exports.pathToServerUri = pathToServerUri; + exports.serverUriToVfsUri = serverUriToVfsUri; +}); diff --git a/src/languageTools/LanguageClient/Connection.js b/src/languageTools/LanguageClient/Connection.js deleted file mode 100644 index 1ad0ad1f77..0000000000 --- a/src/languageTools/LanguageClient/Connection.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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 () { - - - 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 deleted file mode 100644 index d9cc5aff72..0000000000 --- a/src/languageTools/LanguageClient/LanguageClient.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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 }]*/ - - -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 deleted file mode 100644 index 82980d362c..0000000000 --- a/src/languageTools/LanguageClient/NodeToBracketsInterface.js +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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 () { - - - - 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 deleted file mode 100644 index 3ba1a07fb6..0000000000 --- a/src/languageTools/LanguageClient/ProtocolAdapter.js +++ /dev/null @@ -1,398 +0,0 @@ -/* - * 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*/ - - -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 deleted file mode 100644 index f0a3ff94a6..0000000000 --- a/src/languageTools/LanguageClient/ServerUtils.js +++ /dev/null @@ -1,427 +0,0 @@ -/* - * 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 () { - - - 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 deleted file mode 100644 index 0770c34db6..0000000000 --- a/src/languageTools/LanguageClient/Utils.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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*/ - - -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 deleted file mode 100644 index 2fdc6b6094..0000000000 --- a/src/languageTools/LanguageClient/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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 deleted file mode 100644 index 26eed4f4a5..0000000000 --- a/src/languageTools/LanguageClientWrapper.js +++ /dev/null @@ -1,666 +0,0 @@ -/* - * 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) { - - - var ToolingInfo = JSON.parse(require("text!languageTools/ToolingInfo.json")), - MESSAGE_FORMAT = { - BRACKETS: "brackets", - LSP: "lsp" - }; - - function _addTypeInformation(type, params) { - return { - type: type, - params: params - }; - } - - function hasValidProp(obj, prop) { - return (obj && obj[prop] !== undefined && obj[prop] !== null); - } - - function hasValidProps(obj, props) { - var retval = !!obj, - len = props.length, - i; - - for (i = 0; retval && (i < len); i++) { - retval = (retval && obj[props[i]] !== undefined && obj[props[i]] !== null); - } - - return retval; - } - /* - RequestParams creator - sendNotifications/request - */ - function validateRequestParams(type, params) { - var validatedParams = null; - - params = params || {}; - - //Don't validate if the formatting is done by the caller - if (params.format === MESSAGE_FORMAT.LSP) { - return params; - } - - switch (type) { - case ToolingInfo.LANGUAGE_SERVICE.START: - { - if (hasValidProp(params, "rootPaths") || hasValidProp(params, "rootPath")) { - validatedParams = params; - validatedParams.capabilities = validatedParams.capabilities || false; - } - break; - } - case ToolingInfo.FEATURES.CODE_HINTS: - case ToolingInfo.FEATURES.PARAMETER_HINTS: - case ToolingInfo.FEATURES.JUMP_TO_DECLARATION: - case ToolingInfo.FEATURES.JUMP_TO_DEFINITION: - case ToolingInfo.FEATURES.JUMP_TO_IMPL: - { - if (hasValidProps(params, ["filePath", "cursorPos"])) { - validatedParams = params; - } - break; - } - case ToolingInfo.FEATURES.CODE_HINT_INFO: - { - validatedParams = params; - break; - } - case ToolingInfo.FEATURES.FIND_REFERENCES: - { - if (hasValidProps(params, ["filePath", "cursorPos"])) { - validatedParams = params; - validatedParams.includeDeclaration = validatedParams.includeDeclaration || false; - } - break; - } - case ToolingInfo.FEATURES.DOCUMENT_SYMBOLS: - { - if (hasValidProp(params, "filePath")) { - validatedParams = params; - } - break; - } - case ToolingInfo.FEATURES.PROJECT_SYMBOLS: - { - if (hasValidProp(params, "query") && typeof params.query === "string") { - validatedParams = params; - } - break; - } - case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST: - { - validatedParams = params; - } - } - - return validatedParams; - } - - /* - ReponseParams transformer - used by OnNotifications - */ - function validateNotificationParams(type, params) { - var validatedParams = null; - - params = params || {}; - - //Don't validate if the formatting is done by the caller - if (params.format === MESSAGE_FORMAT.LSP) { - return params; - } - - switch (type) { - case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED: - { - if (hasValidProps(params, ["filePath", "fileContent", "languageId"])) { - validatedParams = params; - } - break; - } - case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED: - { - if (hasValidProps(params, ["filePath", "fileContent"])) { - validatedParams = params; - } - break; - } - case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED: - { - if (hasValidProp(params, "filePath")) { - validatedParams = params; - } - break; - } - case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED: - { - if (hasValidProp(params, "filePath")) { - validatedParams = params; - } - break; - } - case ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED: - { - if (hasValidProps(params, ["foldersAdded", "foldersRemoved"])) { - validatedParams = params; - } - break; - } - case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION: - { - validatedParams = params; - } - } - - return validatedParams; - } - - function validateHandler(handler) { - var retval = false; - - if (handler && typeof handler === "function") { - retval = true; - } else { - console.warn("Handler validation failed. Handler should be of type 'function'. Provided handler is of type :", typeof handler); - } - - return retval; - } - - function LanguageClientWrapper(name, path, domainInterface, languages) { - this._name = name; - this._path = path; - this._domainInterface = domainInterface; - this._languages = languages || []; - this._startClient = null; - this._stopClient = null; - this._notifyClient = null; - this._requestClient = null; - this._onRequestHandler = {}; - this._onNotificationHandlers = {}; - this._dynamicCapabilities = {}; - this._serverCapabilities = {}; - - //Initialize with keys for brackets events we want to tap into. - this._onEventHandlers = { - "activeEditorChange": [], - "projectOpen": [], - "beforeProjectClose": [], - "dirtyFlagChange": [], - "documentChange": [], - "fileNameChange": [], - "beforeAppClose": [] - }; - - this._init(); - } - - LanguageClientWrapper.prototype._init = function () { - this._domainInterface.registerMethods([ - { - methodName: ToolingInfo.LANGUAGE_SERVICE.REQUEST, - methodHandle: this._onRequestDelegator.bind(this) - }, - { - methodName: ToolingInfo.LANGUAGE_SERVICE.NOTIFY, - methodHandle: this._onNotificationDelegator.bind(this) - } - ]); - - //create function interfaces - this._startClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.START, true); - this._stopClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.STOP, true); - this._notifyClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.NOTIFY); - this._requestClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.REQUEST, true); - }; - - LanguageClientWrapper.prototype._onRequestDelegator = function (params) { - if (!params || !params.type) { - console.log("Invalid server request"); - return $.Deferred().reject(); - } - - var requestHandler = this._onRequestHandler[params.type]; - if (params.type === ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST) { - return this._registrationShim(params.params, requestHandler); - } - - if (params.type === ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST) { - return this._unregistrationShim(params.params, requestHandler); - } - - if (validateHandler(requestHandler)) { - return requestHandler.call(null, params.params); - } - console.log("No handler provided for server request type : ", params.type); - return $.Deferred().reject(); - - }; - - LanguageClientWrapper.prototype._onNotificationDelegator = function (params) { - if (!params || !params.type) { - console.log("Invalid server notification"); - return; - } - - var notificationHandlers = this._onNotificationHandlers[params.type]; - if (notificationHandlers && Array.isArray(notificationHandlers) && notificationHandlers.length) { - notificationHandlers.forEach(function (handler) { - if (validateHandler(handler)) { - handler.call(null, params.params); - } - }); - } else { - console.log("No handlers provided for server notification type : ", params.type); - } - }; - - LanguageClientWrapper.prototype._request = function (type, params) { - params = validateRequestParams(type, params); - if (params) { - params = _addTypeInformation(type, params); - return this._requestClient(params); - } - - console.log("Invalid Parameters provided for request type : ", type); - return $.Deferred().reject(); - }; - - LanguageClientWrapper.prototype._notify = function (type, params) { - params = validateNotificationParams(type, params); - if (params) { - params = _addTypeInformation(type, params); - this._notifyClient(params); - } else { - console.log("Invalid Parameters provided for notification type : ", type); - } - }; - - LanguageClientWrapper.prototype._addOnRequestHandler = function (type, handler) { - if (validateHandler(handler)) { - this._onRequestHandler[type] = handler; - } - }; - - LanguageClientWrapper.prototype._addOnNotificationHandler = function (type, handler) { - if (validateHandler(handler)) { - if (!this._onNotificationHandlers[type]) { - this._onNotificationHandlers[type] = []; - } - - this._onNotificationHandlers[type].push(handler); - } - }; - - /** - Requests - */ - //start - LanguageClientWrapper.prototype.start = function (params) { - params = validateRequestParams(ToolingInfo.LANGUAGE_SERVICE.START, params); - if (params) { - var self = this; - return this._startClient(params) - .then(function (result) { - self.setServerCapabilities(result.capabilities); - return $.Deferred().resolve(result); - }, function (err) { - return $.Deferred().reject(err); - }); - } - - console.log("Invalid Parameters provided for request type : start"); - return $.Deferred().reject(); - }; - - //shutdown - LanguageClientWrapper.prototype.stop = function () { - return this._stopClient(); - }; - - //restart - LanguageClientWrapper.prototype.restart = function (params) { - var self = this; - return this.stop().then(function () { - return self.start(params); - }); - }; - - /** - textDocument requests - */ - //completion - LanguageClientWrapper.prototype.requestHints = function (params) { - return this._request(ToolingInfo.FEATURES.CODE_HINTS, params) - .then(function(response) { - if(response && response.items && response.items.length) { - logAnalyticsData("CODE_HINTS"); - } - return $.Deferred().resolve(response); - }, function(err) { - return $.Deferred().reject(err); - }); - }; - - //completionItemResolve - LanguageClientWrapper.prototype.getAdditionalInfoForHint = function (params) { - return this._request(ToolingInfo.FEATURES.CODE_HINT_INFO, params); - }; - - //signatureHelp - LanguageClientWrapper.prototype.requestParameterHints = function (params) { - return this._request(ToolingInfo.FEATURES.PARAMETER_HINTS, params) - .then(function(response) { - if (response && response.signatures && response.signatures.length) { - logAnalyticsData("PARAM_HINTS"); - } - return $.Deferred().resolve(response); - }, function(err) { - return $.Deferred().reject(err); - }); - }; - - //gotoDefinition - LanguageClientWrapper.prototype.gotoDefinition = function (params) { - return this._request(ToolingInfo.FEATURES.JUMP_TO_DEFINITION, params) - .then(function(response) { - if(response && response.range) { - logAnalyticsData("JUMP_TO_DEF"); - } - return $.Deferred().resolve(response); - }, function(err) { - return $.Deferred().reject(err); - }); - }; - - //gotoDeclaration - LanguageClientWrapper.prototype.gotoDeclaration = function (params) { - return this._request(ToolingInfo.FEATURES.JUMP_TO_DECLARATION, params); - }; - - //gotoImplementation - LanguageClientWrapper.prototype.gotoImplementation = function (params) { - return this._request(ToolingInfo.FEATURES.JUMP_TO_IMPL, params); - }; - - //findReferences - LanguageClientWrapper.prototype.findReferences = function (params) { - return this._request(ToolingInfo.FEATURES.FIND_REFERENCES, params); - }; - - //documentSymbol - LanguageClientWrapper.prototype.requestSymbolsForDocument = function (params) { - return this._request(ToolingInfo.FEATURES.DOCUMENT_SYMBOLS, params); - }; - - /** - workspace requests - */ - //workspaceSymbol - LanguageClientWrapper.prototype.requestSymbolsForWorkspace = function (params) { - return this._request(ToolingInfo.FEATURES.PROJECT_SYMBOLS, params); - }; - - //These will mostly be callbacks/[done-fail](promises) - /** - Window OnNotifications - */ - //showMessage - LanguageClientWrapper.prototype.addOnShowMessage = function (handler) { - this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.SHOW_MESSAGE, handler); - }; - - //logMessage - LanguageClientWrapper.prototype.addOnLogMessage = function (handler) { - this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.LOG_MESSAGE, handler); - }; - - /** - healthData/logging OnNotifications - */ - //telemetry - LanguageClientWrapper.prototype.addOnTelemetryEvent = function (handler) { - this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.TELEMETRY, handler); - }; - - /** - textDocument OnNotifications - */ - //onPublishDiagnostics - LanguageClientWrapper.prototype.addOnCodeInspection = function (handler) { - this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.DIAGNOSTICS, handler); - }; - - /** - Window OnRequest - */ - - //showMessageRequest - handler must return promise - LanguageClientWrapper.prototype.onShowMessageWithRequest = function (handler) { - this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.SHOW_SELECT_MESSAGE, handler); - }; - - LanguageClientWrapper.prototype.onProjectFoldersRequest = function (handler) { - this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.PROJECT_FOLDERS_REQUEST, handler); - }; - - LanguageClientWrapper.prototype._registrationShim = function (params, handler) { - var self = this; - - var registrations = params.registrations; - registrations.forEach(function (registration) { - var id = registration.id; - self._dynamicCapabilities[id] = registration; - }); - return validateHandler(handler) ? handler(params) : $.Deferred().resolve(); - }; - - LanguageClientWrapper.prototype.onDynamicCapabilityRegistration = function (handler) { - this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST, handler); - }; - - LanguageClientWrapper.prototype._unregistrationShim = function (params, handler) { - var self = this; - - var unregistrations = params.unregistrations; - unregistrations.forEach(function (unregistration) { - var id = unregistration.id; - delete self._dynamicCapabilities[id]; - }); - return validateHandler(handler) ? handler(params) : $.Deferred().resolve(); - }; - - LanguageClientWrapper.prototype.onDynamicCapabilityUnregistration = function (handler) { - this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST, handler); - }; - - /* - Unimplemented OnNotifications - workspace - applyEdit (codeAction, codeLens) - */ - - /** - SendNotifications - */ - - /** - workspace SendNotifications - */ - //didChangeProjectRoots - LanguageClientWrapper.prototype.notifyProjectRootsChanged = function (params) { - this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED, params); - }; - - /** - textDocument SendNotifications - */ - //didOpenTextDocument - LanguageClientWrapper.prototype.notifyTextDocumentOpened = function (params) { - this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED, params); - }; - - //didCloseTextDocument - LanguageClientWrapper.prototype.notifyTextDocumentClosed = function (params) { - this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED, params); - }; - - //didChangeTextDocument - LanguageClientWrapper.prototype.notifyTextDocumentChanged = function (params) { - this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED, params); - }; - - //didSaveTextDocument - LanguageClientWrapper.prototype.notifyTextDocumentSave = function (params) { - this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED, params); - }; - - /** - Custom messages - */ - - //customNotification - LanguageClientWrapper.prototype.sendCustomNotification = function (params) { - this._notify(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION, params); - }; - - LanguageClientWrapper.prototype.onCustomNotification = function (type, handler) { - this._addOnNotificationHandler(type, handler); - }; - - //customRequest - LanguageClientWrapper.prototype.sendCustomRequest = function (params) { - return this._request(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST, params); - }; - - LanguageClientWrapper.prototype.onCustomRequest = function (type, handler) { - this._addOnRequestHandler(type, handler); - }; - - //Handling Brackets Events - LanguageClientWrapper.prototype.addOnEditorChangeHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["activeEditorChange"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addOnProjectOpenHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["projectOpen"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addBeforeProjectCloseHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["beforeProjectClose"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addOnDocumentDirtyFlagChangeHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["dirtyFlagChange"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addOnDocumentChangeHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["documentChange"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addOnFileRenameHandler = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["fileNameChange"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addBeforeAppClose = function (handler) { - if (validateHandler(handler)) { - this._onEventHandlers["beforeAppClose"].push(handler); - } - }; - - LanguageClientWrapper.prototype.addOnCustomEventHandler = function (eventName, handler) { - if (validateHandler(handler)) { - if (!this._onEventHandlers[eventName]) { - this._onEventHandlers[eventName] = []; - } - this._onEventHandlers[eventName].push(handler); - } - }; - - LanguageClientWrapper.prototype.triggerEvent = function (event) { - var eventName = event.type, - eventArgs = arguments; - - if (this._onEventHandlers[eventName] && Array.isArray(this._onEventHandlers[eventName])) { - var handlers = this._onEventHandlers[eventName]; - - handlers.forEach(function (handler) { - if (validateHandler(handler)) { - handler.apply(null, eventArgs); - } - }); - } - }; - - LanguageClientWrapper.prototype.getDynamicCapabilities = function () { - return this._dynamicCapabilities; - }; - - LanguageClientWrapper.prototype.getServerCapabilities = function () { - return this._serverCapabilities; - }; - - LanguageClientWrapper.prototype.setServerCapabilities = function (serverCapabilities) { - this._serverCapabilities = serverCapabilities; - }; - - exports.LanguageClientWrapper = LanguageClientWrapper; - - function logAnalyticsData(typeStrKey) { - var editor = require("editor/EditorManager").getActiveEditor(), - document = editor ? editor.document : null, - language = document ? document.language : null, - languageName = language ? language._name : "", - Metrics = require("utils/Metrics"), - typeStr = typeStrKey; - - Metrics.countEvent( - Metrics.EVENT_TYPE.CODE_HINTS, - "languageServerProtocol", - typeStr + languageName.toLowerCase() - ); - } - - //For unit testting - exports.validateRequestParams = validateRequestParams; - exports.validateNotificationParams = validateNotificationParams; -}); diff --git a/src/languageTools/LanguageTools.js b/src/languageTools/LanguageTools.js deleted file mode 100644 index 513b73d2ac..0000000000 --- a/src/languageTools/LanguageTools.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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) { - - - 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/ToolingInfo.json b/src/languageTools/ToolingInfo.json deleted file mode 100644 index d7457ec6a9..0000000000 --- a/src/languageTools/ToolingInfo.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 deleted file mode 100644 index ad53ae5ed5..0000000000 --- a/src/languageTools/node/RegisterLanguageClientInfo.js +++ /dev/null @@ -1,296 +0,0 @@ -/* - * 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 }]*/ - - -var domainName = "LanguageClientInfo", - LANGUAGE_CLIENT_RELATIVE_PATH_ARRAY = ["languageTools", "LanguageClient", "LanguageClient"], - FORWARD_SLASH = "/", - BACKWARD_SLASH = "\\", - CompletionItemKind = { - Text: 1, - Method: 2, - Function: 3, - Constructor: 4, - Field: 5, - Variable: 6, - Class: 7, - Interface: 8, - Module: 9, - Property: 10, - Unit: 11, - Value: 12, - Enum: 13, - Keyword: 14, - Snippet: 15, - Color: 16, - File: 17, - Reference: 18, - Folder: 19, - EnumMember: 20, - Constant: 21, - Struct: 22, - Event: 23, - Operator: 24, - TypeParameter: 25 - }, - SymbolKind = { - File: 1, - Module: 2, - Namespace: 3, - Package: 4, - Class: 5, - Method: 6, - Property: 7, - Field: 8, - Constructor: 9, - Enum: 10, - Interface: 11, - Function: 12, - Variable: 13, - Constant: 14, - String: 15, - Number: 16, - Boolean: 17, - Array: 18, - Object: 19, - Key: 20, - Null: 21, - EnumMember: 22, - Struct: 23, - Event: 24, - Operator: 25, - TypeParameter: 26 - }, - defaultBracketsCapabilities = { - //brackets default capabilties - "workspace": { - "workspaceFolders": true, - "symbol": { - "dynamicRegistration": false, - "symbolKind": [ - SymbolKind.File, - SymbolKind.Module, - SymbolKind.Namespace, - SymbolKind.Package, - SymbolKind.Class, - SymbolKind.Method, - SymbolKind.Property, - SymbolKind.Field, - SymbolKind.Constructor, - SymbolKind.Enum, - SymbolKind.Interface, - SymbolKind.Function, - SymbolKind.Variable, - SymbolKind.Constant, - SymbolKind.String, - SymbolKind.Number, - SymbolKind.Boolean, - SymbolKind.Array, - SymbolKind.Object, - SymbolKind.Key, - SymbolKind.Null, - SymbolKind.EnumMember, - SymbolKind.Struct, - SymbolKind.Event, - SymbolKind.Operator, - SymbolKind.TypeParameter - ] - } - }, - "textDocument": { - "synchronization": { - "didSave": true - }, - "completion": { - "dynamicRegistration": false, - "completionItem": { - "deprecatedSupport": true, - "documentationFormat": ["plaintext"], - "preselectSupport": true - }, - "completionItemKind": { - "valueSet": [ - CompletionItemKind.Text, - CompletionItemKind.Method, - CompletionItemKind.Function, - CompletionItemKind.Constructor, - CompletionItemKind.Field, - CompletionItemKind.Variable, - CompletionItemKind.Class, - CompletionItemKind.Interface, - CompletionItemKind.Module, - CompletionItemKind.Property, - CompletionItemKind.Unit, - CompletionItemKind.Value, - CompletionItemKind.Enum, - CompletionItemKind.Keyword, - CompletionItemKind.Snippet, - CompletionItemKind.Color, - CompletionItemKind.File, - CompletionItemKind.Reference, - CompletionItemKind.Folder, - CompletionItemKind.EnumMember, - CompletionItemKind.Constant, - CompletionItemKind.Struct, - CompletionItemKind.Event, - CompletionItemKind.Operator, - CompletionItemKind.TypeParameter - ] - }, - "contextSupport": true - }, - "signatureHelp": { - "dynamicRegistration": false, - "signatureInformation": { - "documentationFormat": ["plaintext"] - } - }, - "references": { - "dynamicRegistration": false - }, - "documentSymbol": { - "dynamicRegistration": false, - "symbolKind": { - "valueSet": [ - SymbolKind.File, - SymbolKind.Module, - SymbolKind.Namespace, - SymbolKind.Package, - SymbolKind.Class, - SymbolKind.Method, - SymbolKind.Property, - SymbolKind.Field, - SymbolKind.Constructor, - SymbolKind.Enum, - SymbolKind.Interface, - SymbolKind.Function, - SymbolKind.Variable, - SymbolKind.Constant, - SymbolKind.String, - SymbolKind.Number, - SymbolKind.Boolean, - SymbolKind.Array, - SymbolKind.Object, - SymbolKind.Key, - SymbolKind.Null, - SymbolKind.EnumMember, - SymbolKind.Struct, - SymbolKind.Event, - SymbolKind.Operator, - SymbolKind.TypeParameter - ] - }, - "hierarchicalDocumentSymbolSupport": false - }, - "definition": { - "dynamicRegistration": false - }, - "declaration": { - "dynamicRegistration": false - }, - "typeDefinition": { - "dynamicRegistration": false - }, - "implementation": { - "dynamicRegistration": false - }, - "publishDiagnostics": { - "relatedInformation": true - } - } - }; - -function syncPreferences(prefs) { - global.LanguageClientInfo = global.LanguageClientInfo || {}; - global.LanguageClientInfo.preferences = prefs || global.LanguageClientInfo.preferences || {}; -} - -function initialize(bracketsSourcePath, toolingInfo, resolve) { - if (!bracketsSourcePath || !toolingInfo) { - resolve(true, null); //resolve with err param - } - - var normalizedBracketsSourcePath = bracketsSourcePath.split(BACKWARD_SLASH).join(FORWARD_SLASH), - bracketsSourcePathArray = normalizedBracketsSourcePath.split(FORWARD_SLASH), - languageClientAbsolutePath = bracketsSourcePathArray.concat(LANGUAGE_CLIENT_RELATIVE_PATH_ARRAY).join(FORWARD_SLASH); - - global.LanguageClientInfo = global.LanguageClientInfo || {}; - global.LanguageClientInfo.languageClientPath = languageClientAbsolutePath; - global.LanguageClientInfo.defaultBracketsCapabilities = defaultBracketsCapabilities; - global.LanguageClientInfo.toolingInfo = toolingInfo; - global.LanguageClientInfo.preferences = {}; - - resolve(null, true); //resolve with boolean denoting success -} - -function init(domainManager) { - if (!domainManager.hasDomain(domainName)) { - domainManager.registerDomain(domainName, { - major: 0, - minor: 1 - }); - } - - domainManager.registerCommand( - domainName, - "initialize", - initialize, - true, - "Initialize node environment for Language Client Module", - [ - { - name: "bracketsSourcePath", - type: "string", - description: "Absolute path to the brackets source" - }, - { - name: "toolingInfo", - type: "object", - description: "Tooling Info json to be used by Language Client" - } - ], - [] - ); - - domainManager.registerCommand( - domainName, - "syncPreferences", - syncPreferences, - false, - "Sync language tools preferences for Language Client Module", - [ - { - name: "prefs", - type: "object", - description: "Language tools preferences" - } - ], - [] - ); -} - -exports.init = init; diff --git a/src/languageTools/styles/default_provider_style.css b/src/languageTools/styles/default_provider_style.css deleted file mode 100644 index 7c99828202..0000000000 --- a/src/languageTools/styles/default_provider_style.css +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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; -} - -.codehint-menu { - font-family: SourceCodePro; -} - -.brackets-hints .matched-hint { - font-weight: 500; - color: #437900; -} - -.dark .brackets-hints .matched-hint { - font-weight: 500; - color: #74B120; -} - -#function-hint-container-new { - display: none; - - background: #fff; - position: absolute; - z-index: var(--z-index-parameter-hints); - 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 9a596f3b70..55fa626468 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1004,6 +1004,7 @@ define({ "ERRORS_NO_FILE": "No File Open", "ERRORS_PANEL_TITLE_MULTIPLE": "{0} Problems - {1}", "ERRORS_PANEL_TITLE_MULTIPLE_FIXABLE": "{0} Problems, {1} Fixable - {2}", + "ERRORS_PANEL_TITLE_INFO_SUFFIX": "{0} ({1} Info)", "SINGLE_ERROR": "1 {0} Problem - {1}", "SINGLE_ERROR_FIXABLE": "1 {0} Problem, {1} Fixable - {2}", "MULTIPLE_ERRORS": "{0} {1} Problems - {2}", @@ -1497,7 +1498,7 @@ define({ "EDIT": "Edit", // extensions/default/JavaScriptCodeHints - "CMD_JUMPTO_DEFINITION": "Jump to Definition", + "CMD_JUMPTO_DEFINITION": "Go to Definition", "CMD_SHOW_PARAMETER_HINT": "Show Parameter Hint", "NO_ARGUMENTS": "", "DETECTED_EXCLUSION_TITLE": "JavaScript File Inference Problem", @@ -1734,10 +1735,7 @@ define({ "DESCRIPTION_PHP_TOOLING_CONFIGURATION": "PHP Tooling default configuration settings", "OPEN_PREFERENNCES": "Open Preferences", - //Strings for LanguageTools Preferences - "LANGUAGE_TOOLS_PREFERENCES": "Preferences for Language Tools", - - "FIND_ALL_REFERENCES": "Find All References", + "FIND_ALL_REFERENCES": "Find Usages", "REFERENCES_IN_FILES": "references", "REFERENCE_IN_FILES": "reference", "REFERENCES_NO_RESULTS": "No References available for current cursor position", diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 14c5192767..dd788f6911 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -2142,6 +2142,479 @@ a, img { } } +// LSP hover documentation (rendered inside #quick-view-container) +.lsp-hover-quickview { + text-align: left; + max-width: 520px; + padding: 2px 4px; + font-size: 12.5px; + line-height: 1.5; + color: @bc-text; + word-wrap: break-word; + + .dark & { + color: @dark-bc-text; + } + + // The documentation scrolls; the action footer below it stays pinned and always visible. + .lsp-hover-doc { + max-height: 320px; + overflow-y: auto; + overflow-x: hidden; + } + + // First block is the function/type signature (a ```ts``` fence): the visual focal point. + pre { + margin: 0 0 9px 0; + padding: 8px 11px; + background: @bc-panel-bg-alt; + border: 1px solid rgba(0, 0, 0, 0.10); + border-radius: 6px; + overflow-x: auto; + white-space: pre; + + .dark & { + background: #1c1c1c; + border-color: rgba(255, 255, 255, 0.09); + } + } + + pre code { + font-family: 'SourceCodePro', 'SF Mono', Menlo, Consolas, monospace; + font-size: 12px; + line-height: 1.5; + color: @bc-text-emphasized; + background: none; + padding: 0; + border: 0; + + .dark & { + color: @dark-bc-text-emphasized; + } + } + + // highlight.js token colours for the signature block. The app's global hljs theme is + // github-dark (locked, for the always-dark sidebar); override it within the hover so the + // signature reads well on the light theme too. The chip background follows the editor theme, + // so we use a GitHub-light palette in light mode and a GitHub-dark palette in dark mode. + pre code.hljs { background: none; } + .hljs-keyword, .hljs-literal, .hljs-doctag, .hljs-meta .hljs-keyword { color: #cf222e; } + .hljs-string, .hljs-regexp, .hljs-meta-string { color: #0a3069; } + .hljs-title, .hljs-title.function_, .hljs-title.class_, .hljs-section { color: #6639ba; } + .hljs-built_in, .hljs-type, .hljs-class .hljs-title { color: #953800; } + .hljs-number, .hljs-symbol, .hljs-bullet { color: #0550ae; } + .hljs-attr, .hljs-attribute, .hljs-property, .hljs-variable, .hljs-params { color: @bc-text-emphasized; } + .hljs-comment, .hljs-quote, .hljs-meta { color: #6e7781; } + + .dark & { + .hljs-keyword, .hljs-literal, .hljs-doctag, .hljs-meta .hljs-keyword { color: #ff7b72; } + .hljs-string, .hljs-regexp, .hljs-meta-string { color: #a5d6ff; } + .hljs-title, .hljs-title.function_, .hljs-title.class_, .hljs-section { color: #d2a8ff; } + .hljs-built_in, .hljs-type, .hljs-class .hljs-title { color: #ffa657; } + .hljs-number, .hljs-symbol, .hljs-bullet { color: #79c0ff; } + .hljs-attr, .hljs-attribute, .hljs-property, .hljs-variable, .hljs-params { color: @dark-bc-text-emphasized; } + .hljs-comment, .hljs-quote, .hljs-meta { color: #8b949e; } + } + + // Inline code: parameter names, types, identifiers. + code { + font-family: 'SourceCodePro', 'SF Mono', Menlo, Consolas, monospace; + font-size: 11.5px; + padding: 1px 5px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.06); + color: @bc-text-emphasized; + + .dark & { + background: rgba(255, 255, 255, 0.08); + color: @dark-bc-text-emphasized; + } + } + + p { + margin: 6px 0; + } + p:first-of-type { + margin-top: 0; + } + p:last-child { + margin-bottom: 0; + } + + // JSDoc tags (@param, @returns, ...) arrive as emphasis - present them as crisp accent labels. + em { + font-style: normal; + font-weight: 600; + color: @bc-text-link; + + .dark & { + color: @dark-bc-text-link; + } + } + + strong { + font-weight: 600; + color: @bc-text-emphasized; + .dark & { + color: @dark-bc-text-emphasized; + } + } + + a { + color: @bc-text-link; + text-decoration: none; + .dark & { + color: @dark-bc-text-link; + } + &:hover { + text-decoration: underline; + } + } + + // Separator between signature/sections (rendered from markdown `---`). + hr { + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.08); + margin: 9px 0; + .dark & { + border-top-color: rgba(255, 255, 255, 0.08); + } + } + + ul, ol { + margin: 5px 0; + padding-left: 18px; + } + li { + margin: 2px 0; + } + + // Slim, unobtrusive scrollbar on the scrolling doc that matches both themes. + .lsp-hover-doc::-webkit-scrollbar { + width: 9px; + height: 9px; + } + .lsp-hover-doc::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.18); + border-radius: 5px; + .dark & { + background: rgba(255, 255, 255, 0.18); + } + } + .lsp-hover-doc::-webkit-scrollbar-track { + background: transparent; + } + + // Quick actions (Go to Definition / Find Usages) pinned below the documentation. + .lsp-hover-actions { + display: flex; + flex-wrap: wrap; + gap: 3px; + margin-top: 8px; + padding-top: 7px; + border-top: 1px solid rgba(0, 0, 0, 0.08); + .dark & { + border-top-color: rgba(255, 255, 255, 0.08); + } + } + .lsp-hover-action { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 9px; + border-radius: 5px; + cursor: pointer; + user-select: none; + color: @bc-text-link; + transition: background-color 0.12s ease; + + .dark & { + color: @dark-bc-text-link; + } + &:hover, + &:focus { + outline: none; + background: rgba(0, 0, 0, 0.06); + .dark & { + background: rgba(255, 255, 255, 0.09); + } + } + + // Push this action to the far right of the row (e.g. Find Usages). + &.lsp-hover-action--end { + margin-left: auto; + } + + .lsp-hover-action-icon { + font-size: 11px; + opacity: 0.85; + } + .lsp-hover-action-label { + font-size: 12px; + font-weight: 500; + } + } +} + +// LSP code-hint documentation popup (shown beside the code hint list) +.lsp-hint-doc-popup { + display: none; + position: fixed; + z-index: 10000; + box-sizing: border-box; + width: 360px; + max-height: 320px; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 12px; + font-size: 12.5px; + line-height: 1.5; + word-wrap: break-word; + + background: @bc-panel-bg; + color: @bc-text; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + box-shadow: 0 4px 15px @bc-shadow-large; + + .dark & { + background: @dark-bc-panel-bg; + color: @dark-bc-text; + border-color: rgba(255, 255, 255, 0.10); + box-shadow: 0 4px 15px @dark-bc-shadow-large; + } + + p { margin: 6px 0; } + p:first-child { margin-top: 0; } + p:last-child { margin-bottom: 0; } + + pre { + margin: 6px 0; + padding: 8px 10px; + background: @bc-panel-bg-alt; + border: 1px solid rgba(0, 0, 0, 0.10); + border-radius: 5px; + overflow-x: auto; + white-space: pre; + .dark & { + background: #1c1c1c; + border-color: rgba(255, 255, 255, 0.09); + } + } + pre code { + font-family: 'SourceCodePro', 'SF Mono', Menlo, Consolas, monospace; + font-size: 12px; + color: @bc-text-emphasized; + background: none; + padding: 0; + .dark & { color: @dark-bc-text-emphasized; } + } + code { + font-family: 'SourceCodePro', 'SF Mono', Menlo, Consolas, monospace; + font-size: 11.5px; + padding: 1px 5px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.06); + color: @bc-text-emphasized; + .dark & { + background: rgba(255, 255, 255, 0.08); + color: @dark-bc-text-emphasized; + } + } + em { font-style: normal; font-weight: 600; color: @bc-text-link; .dark & { color: @dark-bc-text-link; } } + a { color: @bc-text-link; text-decoration: none; .dark & { color: @dark-bc-text-link; } } + a:hover { text-decoration: underline; } + hr { + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.08); + margin: 8px 0; + .dark & { border-top-color: rgba(255, 255, 255, 0.08); } + } + ul, ol { margin: 5px 0; padding-left: 18px; } + + &::-webkit-scrollbar { width: 9px; } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.18); border-radius: 5px; + .dark & { background: rgba(255, 255, 255, 0.18); } + } + &::-webkit-scrollbar-track { background: transparent; } +} + +// LSP / language-tools code hints. Moved here from languageTools/styles/default_provider_style.css +// now that languageTools is a core module loaded at boot (no per-extension stylesheet). Covers the +// completion list rows, the inline signature, the type/keyword/doc decorations and the +// parameter-hint container. +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; + + .dark & { + color: #696969 !important; + } +} + +.highlight .brackets-hints-type-details { + display: none; +} + +.hint-description { + display: none; + color: #d4d4d4; + word-wrap: break-word; + white-space: normal; + box-sizing: border-box; + + .dark & { + color: #ccc; + } +} + +.highlight .hint-description { + display: block; + color: #6495ed !important; +} + +.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; + + .dark & { + color: #ccc; + } +} + +.highlight .hint-doc { + display: -webkit-box; +} + +.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; + color: #437900; + + .dark & { + color: #74B120; + } +} + +.codehint-menu { + font-family: SourceCodePro; +} + +// LSP completion signature (VS Code style). The list keeps a STABLE width and each row is a flex +// line: the label (flexible, truncates with ellipsis) and the highlighted row's signature +// (fixed-size) sit side-by-side, so they never overlap and arrow-navigation never resizes the +// popup. Scoped to `.lsp-hints` menus so other providers are unaffected. The full signature is in +// the side doc popup. +.codehint-menu.lsp-hints .dropdown-menu { + // Fixed width so the list never resizes while navigating (the Bootstrap dropdown is + // shrink-to-fit, so a min-width floor isn't enough - it would still grow per row). + width: 380px; + min-width: 380px; + max-width: 380px; +} + +// Make the the flex container so the row template's whitespace text nodes collapse (a +// block-level .codehint-item would otherwise add an extra line box, making rows ~2x tall). +.codehint-menu.lsp-hints li a { + display: flex; + align-items: center; +} + +.codehint-menu.lsp-hints .codehint-item { + display: flex; + flex: 1 1 auto; + min-width: 0; + align-items: center; +} + +.codehint-menu.lsp-hints .codehint-item > .brackets-hints { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lsp-hint-sig { + display: none; +} + +.highlight .lsp-hint-sig { + display: block; + flex: 0 0 auto; + margin-left: 12px; + max-width: 55%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-style: italic; + font-size: 11px; + color: rgba(0, 0, 0, 0.5); + + .dark & { + color: rgba(255, 255, 255, 0.65); + } +} + +#function-hint-container-new { + display: none; + background: #fff; + position: absolute; + z-index: var(--z-index-parameter-hints); + 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); + + .dark & { + background: #000; + border: 1px solid rgba(255, 255, 255, 0.15); + color: #fff; + 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; +} + // selection view #selection-view-container { display: none; diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index c2bc88733c..fdec07a22b 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -670,6 +670,30 @@ a:focus { box-shadow: 0 3px 9px @dark-bc-shadow; } + // Opaque, theme-matched scrollbar. The app-wide default uses a transparent track, which on + // a floating popup lets the editor behind show through the scroll gutter; match the menu + // background instead so the popup reads as one solid surface. + &::-webkit-scrollbar { + width: 9px; + } + &::-webkit-scrollbar-track { + background-color: @bc-menu-bg; + + .dark & { + background-color: @dark-bc-menu-bg; + } + } + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.24); + border-radius: 5px; + border: 2px solid @bc-menu-bg; + + .dark & { + background-color: rgba(255, 255, 255, 0.24); + border-color: @dark-bc-menu-bg; + } + } + // Styles used for inlinemenu widget header li.inlinemenu-header a { background-color: @bc-bg-tool-bar; diff --git a/src/thirdparty/licences/typescript.markdown b/src/thirdparty/licences/typescript.markdown new file mode 100644 index 0000000000..edc24fd6e1 --- /dev/null +++ b/src/thirdparty/licences/typescript.markdown @@ -0,0 +1,55 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/src/thirdparty/licences/vtsls.markdown b/src/thirdparty/licences/vtsls.markdown new file mode 100644 index 0000000000..8c9834d896 --- /dev/null +++ b/src/thirdparty/licences/vtsls.markdown @@ -0,0 +1,45 @@ +MIT License + +Copyright (c) 2023 yioneko + +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. + +## Some of the code is copied or derived from VSCode[https://github.com/microsoft/vscode], which is subject to the following copyright notice: + +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +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. diff --git a/test/SpecRunner.js b/test/SpecRunner.js index 99bb6e82d6..9a3bc6068e 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -281,11 +281,7 @@ define(function (require, exports, module) { //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"); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index 97d84344be..7a50deccb5 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -126,6 +126,7 @@ define(function (require, exports, module) { require("spec/Generic-integ-test"); require("spec/spacing-auto-detect-integ-test"); require("spec/LocalizationUtils-test"); + require("spec/TabstopManager-test"); require("spec/ScrollTrackHandler-integ-test"); // Integrated extension tests require("spec/Extn-RemoteFileAdapter-integ-test"); @@ -149,6 +150,5 @@ define(function (require, exports, module) { // pro test suite optional components require("./pro-test-suite"); // todo TEST_MODERN - // require("spec/LanguageTools-test"); LSP tests. disabled for now // require("spec/Menu-native-integ-test"); evaluate after we have native menus in os installed builds }); diff --git a/test/spec/CodeHint-integ-test.js b/test/spec/CodeHint-integ-test.js index 3f18bf450c..8e35687120 100644 --- a/test/spec/CodeHint-integ-test.js +++ b/test/spec/CodeHint-integ-test.js @@ -88,7 +88,13 @@ define(function (require, exports, module) { // Note: these don't request hint results - they only examine hints that might already be open function expectNoHints() { var codeHintList = CodeHintManager._getCodeHintList(); - expect(codeHintList).toBeFalsy(); + // "No hints" means nothing is shown to the user. On the desktop build the LSP provider + // (registered at higher priority than the built-in JS hinter) may open a hint session for + // contexts the browser hinter suppresses outright - e.g. inside a regular expression - but + // vtsls returns zero completions there, so the resulting list is empty and never opened. + // So no hints are shown when the list is either absent, or present but closed and empty. + var noHintsShown = !codeHintList || (!codeHintList.isOpen() && codeHintList.hints.length === 0); + expect(noHintsShown).toBe(true); } function expectSomeHints() { diff --git a/test/spec/EditorCommandHandlers-integ-test.js b/test/spec/EditorCommandHandlers-integ-test.js index 0eec3fe27d..1cf50e88df 100644 --- a/test/spec/EditorCommandHandlers-integ-test.js +++ b/test/spec/EditorCommandHandlers-integ-test.js @@ -203,10 +203,22 @@ define(function (require, exports, module) { await awaitsForDone(promise, "Jump To Definition"); selection = myEditor.getSelection(); - expect(fixSel(selection)).toEql(fixSel({ - start: {line: 0, ch: 9}, - end: {line: 0, ch: 15} - })); + if (window.Phoenix.isNativeApp) { + // Desktop has the LSP server (vtsls), which now provides jump-to-definition + // for JavaScript (higher priority than the built-in Tern provider). vtsls + // returns the full declaration range of testMe and we jump to its start + // (the `function` keyword), giving a collapsed cursor at {0,0} - whereas Tern + // selects the identifier name. Both correctly land on the testMe definition. + expect(fixSel(selection)).toEql(fixSel({ + start: {line: 0, ch: 0}, + end: {line: 0, ch: 0} + })); + } else { + expect(fixSel(selection)).toEql(fixSel({ + start: {line: 0, ch: 9}, + end: {line: 0, ch: 15} + })); + } }); }); diff --git a/test/spec/Extn-ESLint-integ-test.js b/test/spec/Extn-ESLint-integ-test.js index 9f21716bac..0317659e94 100644 --- a/test/spec/Extn-ESLint-integ-test.js +++ b/test/spec/Extn-ESLint-integ-test.js @@ -264,8 +264,20 @@ define(function (require, exports, module) { it("should not lint jsx file as ESLint v8 is not configured for react lint", async function () { await _openProjectFile("react.jsx"); await awaits(100); // Just wait for some time to prevent any false linter runs - await _waitForProblemsPanelVisible(false); - expect($("#status-inspection").hasClass("inspection-disabled")).toBeTrue(); + if (window.Phoenix.isNativeApp) { + // On desktop the LSP server (vtsls) provides JavaScript/JSX diagnostics, so a .jsx + // file IS linted by the LSP even though ESLint v8 is not configured for react - + // the inspector is active (not disabled). ESLint itself still contributes nothing. + await awaitsFor(()=>{ + return !$("#status-inspection").hasClass("inspection-disabled"); + }, "jsx inspection to be active via the LSP", 15000); + expect($("#status-inspection").hasClass("inspection-disabled")).toBeFalse(); + } else { + // In the browser build there is no LSP, and ESLint v8 declines jsx, so nothing + // lints the file - the inspector is disabled. + await _waitForProblemsPanelVisible(false); + expect($("#status-inspection").hasClass("inspection-disabled")).toBeTrue(); + } }, 30000); it("should not show JSHint in desktop app if ESLint is active", async function () { diff --git a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js b/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js deleted file mode 100644 index 0f42ff886b..0000000000 --- a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 deleted file mode 100644 index 52c8ce0d28..0000000000 --- a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/main.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 deleted file mode 100644 index cc25d2ac62..0000000000 --- a/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "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 deleted file mode 100644 index 95050e7e0b..0000000000 --- a/test/spec/LanguageTools-test-files/clients/FeatureClient/client.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8243d14fd5..0000000000 --- a/test/spec/LanguageTools-test-files/clients/FeatureClient/main.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 deleted file mode 100644 index 15ae4c8de3..0000000000 --- a/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/client.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5889e5a3b3..0000000000 --- a/test/spec/LanguageTools-test-files/clients/InterfaceTestClient/main.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 deleted file mode 100644 index 9dd7784874..0000000000 --- a/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/client.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 deleted file mode 100644 index c4160a98cf..0000000000 --- a/test/spec/LanguageTools-test-files/clients/LoadSimpleClient/main.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 deleted file mode 100644 index 2f5c22c2f6..0000000000 --- a/test/spec/LanguageTools-test-files/clients/ModuleTestClient/client.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 deleted file mode 100644 index b3507f9d88..0000000000 --- a/test/spec/LanguageTools-test-files/clients/ModuleTestClient/main.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 deleted file mode 100644 index d038105cae..0000000000 --- a/test/spec/LanguageTools-test-files/clients/OptionsTestClient/client.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 deleted file mode 100644 index 67610b9f0e..0000000000 --- a/test/spec/LanguageTools-test-files/clients/OptionsTestClient/main.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8de75dcb4d..0000000000 --- a/test/spec/LanguageTools-test-files/project/sample1.txt +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 9289cdf260..0000000000 --- a/test/spec/LanguageTools-test-files/project/sample2.txt +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 2e0358eab1..0000000000 --- a/test/spec/LanguageTools-test-files/server/lsp-test-server/main.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 deleted file mode 100644 index 17b9af0423..0000000000 --- a/test/spec/LanguageTools-test-files/server/lsp-test-server/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devDependencies": { - "vscode-languageserver": "^5.3.0-next.1" - } -} diff --git a/test/spec/LanguageTools-test.js b/test/spec/LanguageTools-test.js deleted file mode 100644 index c67c77277c..0000000000 --- a/test/spec/LanguageTools-test.js +++ /dev/null @@ -1,1599 +0,0 @@ -/* - * 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) { - - - // 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); - }); - }); - }); - }); -}); diff --git a/test/spec/QuickViewManager-test.js b/test/spec/QuickViewManager-test.js index e14a5804c6..16bc27a1ed 100644 --- a/test/spec/QuickViewManager-test.js +++ b/test/spec/QuickViewManager-test.js @@ -40,6 +40,7 @@ define(function (require, exports, module) { editor, testFile = "test.css", testFileJS = "test.js", + testFileHTML = "test.html", oldFile; beforeAll(async function () { @@ -260,20 +261,26 @@ define(function (require, exports, module) { expect(popoverInfo.content.find("#blinker-fluid").length).toBe(0); }); - it("should register and unregister preview provider for js language", async function () { - QuickViewManager.registerQuickViewProvider(provider, ["javascript"]); + it("should register and unregister preview provider for a single language", async function () { + // This test exercises QuickViewManager's per-language registration (register for one + // language vs "all"). It deliberately uses HTML rather than JavaScript: on the desktop + // build, JS/TS files also carry the LSP hover provider, whose previews would obscure + // what this test checks. HTML is not served by the LSP, so the provider under test is + // the only one in play and the assertions hold on both desktop and browser builds. + QuickViewManager.registerQuickViewProvider(provider, ["html"]); + // Open file is CSS (testFile from beforeEach); an html-only provider must not preview. let popoverInfo = await getPopoverAtPos(4, 14); expect(popoverInfo.content.find("#blinker-fluid").length).toBe(0); - await awaitsForDone(SpecRunnerUtils.openProjectFiles([testFileJS]), "open test file: " + testFileJS); + await awaitsForDone(SpecRunnerUtils.openProjectFiles([testFileHTML]), "open test file: " + testFileHTML); popoverInfo = await getPopoverAtPos(4, 14); expect(popoverInfo.content.find("#blinker-fluid").length).toBe(1); expect(line.length > 1).toBeTrue(); expect(pos).toEql({ line: 4, ch: 14 }); - QuickViewManager.removeQuickViewProvider(provider, ["javascript"]); + QuickViewManager.removeQuickViewProvider(provider, ["html"]); popoverInfo = await getPopoverAtPos(4, 14); expect(popoverInfo).toBe(null); }); diff --git a/test/spec/TabstopManager-test.js b/test/spec/TabstopManager-test.js new file mode 100644 index 0000000000..d57dd81260 --- /dev/null +++ b/test/spec/TabstopManager-test.js @@ -0,0 +1,282 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global describe, it, expect, afterEach*/ + +define(function (require, exports, module) { + const TabstopManager = require("editor/TabstopManager"), + SpecRunnerUtils = require("spec/SpecRunnerUtils"); + + describe("unit:TabstopManager", function () { + + // Convenience: stops as [number, start, end] tuples for terse comparisons. + function tuples(stops) { + return stops.map(function (s) { + return [s.number, s.start, s.end]; + }); + } + + describe("parseSnippet - plain text", function () { + it("should leave text without tab-stops untouched and report no stops", function () { + const parsed = TabstopManager.parseSnippet("console.log"); + expect(parsed.text).toBe("console.log"); + expect(parsed.stops).toEqual([]); + }); + + it("should handle an empty snippet", function () { + const parsed = TabstopManager.parseSnippet(""); + expect(parsed.text).toBe(""); + expect(parsed.stops).toEqual([]); + }); + }); + + describe("parseSnippet - simple tab-stops", function () { + it("should strip a trailing $1 and record an empty stop at its offset", function () { + const parsed = TabstopManager.parseSnippet('import { getFromIndex$1 } from "./db";'); + expect(parsed.text).toBe('import { getFromIndex } from "./db";'); + // "$1" sits right after "getFromIndex" (offset 21) + expect(tuples(parsed.stops)).toEqual([[1, 21, 21]]); + }); + + it("should strip $0 and record it as the (only) stop", function () { + const parsed = TabstopManager.parseSnippet("doThing()$0"); + expect(parsed.text).toBe("doThing()"); + expect(tuples(parsed.stops)).toEqual([[0, 9, 9]]); + }); + + it("should support ${0} brace form", function () { + const parsed = TabstopManager.parseSnippet("a${0}b"); + expect(parsed.text).toBe("ab"); + expect(tuples(parsed.stops)).toEqual([[0, 1, 1]]); + }); + }); + + describe("parseSnippet - ordering", function () { + it("should order positive stops ascending with $0 last", function () { + const parsed = TabstopManager.parseSnippet("$2 $1 $0 $3"); + // text is " " (each stop is empty, separated by single spaces) + expect(parsed.text).toBe(" "); + expect(tuples(parsed.stops)).toEqual([ + [1, 1, 1], + [2, 0, 0], + [3, 3, 3], + [0, 2, 2] + ]); + }); + + it("should keep the first occurrence offset for a repeated (mirror) stop number", function () { + const parsed = TabstopManager.parseSnippet("$1-$1"); + expect(parsed.text).toBe("-"); + // first $1 at offset 0, second occurrence ignored for placement + expect(tuples(parsed.stops)).toEqual([[1, 0, 0]]); + }); + }); + + describe("parseSnippet - placeholders", function () { + it("should keep placeholder default text and span it as the stop range", function () { + const parsed = TabstopManager.parseSnippet("connect(${1:host}, ${2:port})$0"); + expect(parsed.text).toBe("connect(host, port)"); + expect(tuples(parsed.stops)).toEqual([ + [1, 8, 12], // "host" + [2, 14, 18], // "port" + [0, 19, 19] // final caret at end + ]); + }); + + it("should expand a nested placeholder", function () { + const parsed = TabstopManager.parseSnippet("${1:a ${2:b} c}"); + expect(parsed.text).toBe("a b c"); + expect(tuples(parsed.stops)).toEqual([ + [1, 0, 5], // whole "a b c" + [2, 2, 3] // inner "b" + ]); + }); + }); + + describe("parseSnippet - choices", function () { + it("should use the first choice as the inserted/selected text", function () { + const parsed = TabstopManager.parseSnippet("type: ${1|number,string,boolean|}"); + expect(parsed.text).toBe("type: number"); + expect(tuples(parsed.stops)).toEqual([[1, 6, 12]]); + }); + }); + + describe("parseSnippet - variables", function () { + it("should drop an unresolved bare variable", function () { + const parsed = TabstopManager.parseSnippet("name: $TM_FILENAME!"); + expect(parsed.text).toBe("name: !"); + expect(parsed.stops).toEqual([]); + }); + + it("should drop an unresolved ${VAR} but keep its default", function () { + const parsed = TabstopManager.parseSnippet("${TM_FILENAME:untitled}.txt"); + expect(parsed.text).toBe("untitled.txt"); + expect(parsed.stops).toEqual([]); + }); + }); + + describe("parseSnippet - escapes", function () { + it("should treat \\$ as a literal dollar, not a tab-stop", function () { + const parsed = TabstopManager.parseSnippet("cost is \\$1 today"); + expect(parsed.text).toBe("cost is $1 today"); + expect(parsed.stops).toEqual([]); + }); + + it("should unescape \\} and \\\\", function () { + const parsed = TabstopManager.parseSnippet("a\\}b\\\\c"); + expect(parsed.text).toBe("a}b\\c"); + expect(parsed.stops).toEqual([]); + }); + }); + + describe("parseSnippet - multi-line", function () { + it("should preserve newlines and report offsets across them", function () { + const parsed = TabstopManager.parseSnippet("if ($1) {\n $0\n}"); + expect(parsed.text).toBe("if () {\n \n}"); + expect(tuples(parsed.stops)).toEqual([ + [1, 4, 4], // inside the parens on line 1 + [0, 12, 12] // indented body on line 2 + ]); + }); + }); + + // insertSnippet drives the real editor: text replacement, caret/selection placement and the + // marker-backed Tab/Shift-Tab session. Uses a mock editor (a real Editor + CodeMirror), which + // runs in the unit category like Editor-test. + describe("insertSnippet - editor session", function () { + let myDocument, myEditor; + + function createTestEditor(content) { + const mocks = SpecRunnerUtils.createMockEditor(content || "", "javascript"); + myDocument = mocks.doc; + myEditor = mocks.editor; + } + + // Reach the live Tab-session keymap CodeMirror is using, so we exercise the exact + // bindings insertSnippet installed rather than re-implementing navigation here. + function sessionKeymap() { + return (myEditor._codeMirror.state.keyMaps || []).filter(function (m) { + return m.name === "tabstop-session"; + })[0]; + } + function pressTab() { + sessionKeymap().Tab(myEditor._codeMirror); + } + function pressShiftTab() { + sessionKeymap()["Shift-Tab"](myEditor._codeMirror); + } + + const ORIGIN = { line: 0, ch: 0 }; + + afterEach(function () { + TabstopManager.endSession(); // before destroy so no marker ops run on a dead editor + if (myEditor) { + SpecRunnerUtils.destroyMockEditor(myDocument); + myEditor = null; + myDocument = null; + } + }); + + it("should insert plain text and place the caret at the single stop (no session)", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "log($1)", ORIGIN, ORIGIN); + expect(myDocument.getText()).toBe("log()"); + const cursor = myEditor.getCursorPos(); + expect([cursor.line, cursor.ch]).toEqual([0, 4]); + expect(myEditor.getSelectedText()).toBe(""); + expect(TabstopManager.hasActiveSession()).toBe(false); + }); + + it("should select a single placeholder's default text for type-over", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "foo(${1:bar})", ORIGIN, ORIGIN); + expect(myDocument.getText()).toBe("foo(bar)"); + expect(myEditor.getSelectedText()).toBe("bar"); + expect(TabstopManager.hasActiveSession()).toBe(false); + }); + + it("should expand the import-completion snippet without a literal $1", function () { + createTestEditor("import getfromi"); + TabstopManager.insertSnippet(myEditor, 'import { getFromIndex$1 } from "./db";', + ORIGIN, { line: 0, ch: 15 }); + expect(myDocument.getText()).toBe('import { getFromIndex } from "./db";'); + const cursor = myEditor.getCursorPos(); + expect([cursor.line, cursor.ch]).toEqual([0, 21]); // right after getFromIndex + expect(TabstopManager.hasActiveSession()).toBe(false); + }); + + it("should start a session and navigate stops with Tab, ending at $0", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "connect(${1:host}, ${2:port})$0", ORIGIN, ORIGIN); + expect(myDocument.getText()).toBe("connect(host, port)"); + expect(myEditor.getSelectedText()).toBe("host"); + expect(TabstopManager.hasActiveSession()).toBe(true); + + pressTab(); + expect(myEditor.getSelectedText()).toBe("port"); + expect(TabstopManager.hasActiveSession()).toBe(true); + + pressTab(); + // landed on $0 (end of text) - caret only, session ends + const cursor = myEditor.getCursorPos(); + expect([cursor.line, cursor.ch]).toEqual([0, 19]); + expect(myEditor.getSelectedText()).toBe(""); + expect(TabstopManager.hasActiveSession()).toBe(false); + }); + + it("should navigate backwards with Shift-Tab", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b} $0", ORIGIN, ORIGIN); + expect(myEditor.getSelectedText()).toBe("a"); + pressTab(); + expect(myEditor.getSelectedText()).toBe("b"); + pressShiftTab(); + expect(myEditor.getSelectedText()).toBe("a"); + expect(TabstopManager.hasActiveSession()).toBe(true); + }); + + it("should end the session on Esc", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "${1:a} ${2:b}", ORIGIN, ORIGIN); + expect(TabstopManager.hasActiveSession()).toBe(true); + sessionKeymap().Esc(myEditor._codeMirror); + expect(TabstopManager.hasActiveSession()).toBe(false); + }); + + it("should keep stops correct when text is inserted above (markers follow edits)", function () { + createTestEditor(""); + TabstopManager.insertSnippet(myEditor, "fn(${1:a}, ${2:b})", ORIGIN, ORIGIN); + expect(myEditor.getSelectedText()).toBe("a"); + // Simulate an auto-import line being added above the snippet after insertion. + myEditor.document.replaceRange("import x;\n", ORIGIN); + pressTab(); + const sel = myEditor.getSelection(); + expect(myEditor.getSelectedText()).toBe("b"); + expect(sel.start.line).toBe(1); // snippet now lives on line 1 + }); + + it("should replace the given range, not just insert at the cursor", function () { + createTestEditor("foo.barbaz"); + TabstopManager.insertSnippet(myEditor, "log($1)", { line: 0, ch: 4 }, { line: 0, ch: 7 }); + expect(myDocument.getText()).toBe("foo.log()baz"); + }); + }); + }); +}); diff --git a/test/spec/TypeScriptSupport-test-files/hover-js/sample.js b/test/spec/TypeScriptSupport-test-files/hover-js/sample.js new file mode 100644 index 0000000000..1252138258 --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/hover-js/sample.js @@ -0,0 +1,7 @@ +// Fixture for the LSP hover quick-action tests: a symbol with a definition (line 1) and usages. +function greetUser(userName) { + return "Hi " + userName; +} + +greetUser("alpha"); +greetUser("bravo"); diff --git a/test/spec/TypeScriptSupport-test-files/hover-ts/sample.ts b/test/spec/TypeScriptSupport-test-files/hover-ts/sample.ts new file mode 100644 index 0000000000..dce360708d --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/hover-ts/sample.ts @@ -0,0 +1,7 @@ +// Fixture for the LSP hover quick-action tests: a symbol with a definition (line 0) and usages. +export function greetUser(userName: string): string { + return "Hi " + userName; +} + +greetUser("alpha"); +greetUser("bravo"); diff --git a/test/spec/TypeScriptSupport-test-files/hover-ts/tsconfig.json b/test/spec/TypeScriptSupport-test-files/hover-ts/tsconfig.json new file mode 100644 index 0000000000..1c6ce5465a --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/hover-ts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": false + }, + "include": ["**/*.ts"] +} diff --git a/test/spec/TypeScriptSupport-test-files/js-checkjs/implicit.js b/test/spec/TypeScriptSupport-test-files/js-checkjs/implicit.js new file mode 100644 index 0000000000..c0fa3d0b46 --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/js-checkjs/implicit.js @@ -0,0 +1,5 @@ +// Same untyped parameter as js-plain, but this project opts into type-checking via jsconfig +// (checkJs + noImplicitAny), so the LSP SHOULD report the implicit "any". +export function identity(value) { + return value; +} diff --git a/test/spec/TypeScriptSupport-test-files/js-checkjs/jsconfig.json b/test/spec/TypeScriptSupport-test-files/js-checkjs/jsconfig.json new file mode 100644 index 0000000000..6228b984f0 --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/js-checkjs/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "noImplicitAny": true + }, + "include": ["**/*.js"] +} diff --git a/test/spec/TypeScriptSupport-test-files/js-plain/implicit.js b/test/spec/TypeScriptSupport-test-files/js-plain/implicit.js new file mode 100644 index 0000000000..a34a727b30 --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/js-plain/implicit.js @@ -0,0 +1,5 @@ +// Plain JavaScript (no jsconfig/tsconfig, no @ts-check). The untyped parameter would be an +// "implicit any" under type-checking, but a pure-JS project must NOT be nagged about it. +export function identity(value) { + return value; +} diff --git a/test/spec/TypeScriptSupport-test-files/ts/type-error.ts b/test/spec/TypeScriptSupport-test-files/ts/type-error.ts new file mode 100644 index 0000000000..d721e207bb --- /dev/null +++ b/test/spec/TypeScriptSupport-test-files/ts/type-error.ts @@ -0,0 +1,6 @@ +// A deliberate TypeScript type error for the LSP integration test. +const aNumber: number = "this is a string"; + +export function getValue(): number { + return aNumber; +}