diff --git a/check-lsp-ready.js b/check-lsp-ready.js new file mode 100755 index 0000000000..944d2a1933 --- /dev/null +++ b/check-lsp-ready.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Quick check script to verify TypeScript LSP is ready + * Run: node check-lsp-ready.js + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('╔════════════════════════════════════════════════════════════╗'); +console.log('║ TypeScript LSP Integration - Readiness Check ║'); +console.log('╚════════════════════════════════════════════════════════════╝\n'); + +let allGood = true; + +// Check 1: LSP Infrastructure Enabled +console.log('1. LSP Infrastructure'); +const clientLoaderPath = './src/languageTools/ClientLoader.js'; +const clientLoaderContent = fs.readFileSync(clientLoaderPath, 'utf8'); +const lspEnabled = clientLoaderContent.includes('initDomainAndHandleNodeCrash();') && + !clientLoaderContent.includes('//initDomainAndHandleNodeCrash();'); +console.log(' Status:', lspEnabled ? '✓ Enabled' : '✗ Disabled'); +if (!lspEnabled) allGood = false; + +// Check 2: Extension Registered +console.log('\n2. TypeScriptLanguageServer Extension'); +const extensionsPath = './src/extensions/default/DefaultExtensions.json'; +const extensionsContent = fs.readFileSync(extensionsPath, 'utf8'); +const registered = extensionsContent.includes('"TypeScriptLanguageServer"'); +console.log(' Registered:', registered ? '✓ Yes' : '✗ No'); +if (!registered) allGood = false; + +// Check 3: Extension Files +console.log('\n3. Extension Files'); +const mainJs = './src/extensions/default/TypeScriptLanguageServer/main.js'; +const clientJs = './src/extensions/default/TypeScriptLanguageServer/client.js'; +console.log(' main.js:', fs.existsSync(mainJs) ? '✓ Exists' : '✗ Missing'); +console.log(' client.js:', fs.existsSync(clientJs) ? '✓ Exists' : '✗ Missing'); +if (!fs.existsSync(mainJs) || !fs.existsSync(clientJs)) allGood = false; + +// Check 4: TypeScript Language Server +console.log('\n4. TypeScript Language Server'); +const serverPath = './node_modules/.bin/typescript-language-server'; +const serverExists = fs.existsSync(serverPath); +console.log(' Installed:', serverExists ? '✓ Yes' : '✗ No'); + +if (serverExists) { + try { + const version = execSync(serverPath + ' --version', { encoding: 'utf8' }).trim(); + console.log(' Version:', version); + } catch (err) { + console.log(' Version: ✗ Cannot execute'); + allGood = false; + } +} else { + allGood = false; +} + +// Check 5: Client Path Resolution +console.log('\n5. Path Resolution in client.js'); +const clientContent = fs.readFileSync(clientJs, 'utf8'); +const hasCorrectPath = clientContent.includes('../../../../node_modules/.bin/typescript-language-server'); +console.log(' Path:', hasCorrectPath ? '✓ Correct (4 levels up)' : '✗ Wrong'); +if (!hasCorrectPath) allGood = false; + +// Check 6: Simulate Path Resolution +console.log('\n6. Path Resolution Test'); +const clientDir = path.dirname(path.resolve(clientJs)); +const resolvedPath = path.resolve(clientDir, '../../../../node_modules/.bin/typescript-language-server'); +const pathCorrect = fs.existsSync(resolvedPath); +console.log(' From:', clientDir); +console.log(' To:', resolvedPath); +console.log(' Valid:', pathCorrect ? '✓ Yes' : '✗ No'); +if (!pathCorrect) allGood = false; + +// Summary +console.log('\n' + '═'.repeat(60)); +if (allGood) { + console.log('✓✓✓ ALL CHECKS PASSED ✓✓✓'); + console.log('\nTypeScript LSP is ready! To test:'); + console.log(' 1. npm run serve'); + console.log(' 2. Open http://localhost:8000/src in browser'); + console.log(' 3. Check browser console for initialization messages'); + console.log(' 4. Open a .js file and test code completion'); +} else { + console.log('✗✗✗ SOME CHECKS FAILED ✗✗✗'); + console.log('\nPlease fix the issues above before testing.'); +} +console.log('═'.repeat(60)); + +process.exit(allGood ? 0 : 1); diff --git a/package-lock.json b/package-lock.json index 06055874e4..bc9d1cc945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,8 @@ "requirejs": "^2.3.7", "tern": "^0.24.3", "tinycolor2": "^1.4.2", + "typescript": "^5.9.3", + "typescript-language-server": "^5.1.1", "underscore": "^1.13.4" }, "devDependencies": { @@ -446,6 +448,20 @@ "node": ">=8" } }, + "node_modules/@commitlint/load/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@commitlint/message": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-16.2.1.tgz", @@ -12144,16 +12160,28 @@ "dev": true }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "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": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-language-server": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.1.tgz", + "integrity": "sha512-KFOvyFxN5YCTrS1i3tl543kF+yjA3Ih7IQbWZm9b5LZ21LBdSigSduMgqqMDXmfkm/fIkm3XR914lC3mPAuMag==", + "license": "Apache-2.0", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=20" } }, "node_modules/typical": { @@ -13243,6 +13271,12 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true } } }, @@ -22665,10 +22699,14 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" + }, + "typescript-language-server": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.1.tgz", + "integrity": "sha512-KFOvyFxN5YCTrS1i3tl543kF+yjA3Ih7IQbWZm9b5LZ21LBdSigSduMgqqMDXmfkm/fIkm3XR914lC3mPAuMag==" }, "typical": { "version": "7.3.0", diff --git a/package.json b/package.json index c1fd82627e..beb6d89a73 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,8 @@ "requirejs": "^2.3.7", "tern": "^0.24.3", "tinycolor2": "^1.4.2", + "typescript": "^5.9.3", + "typescript-language-server": "^5.1.1", "underscore": "^1.13.4" } } diff --git a/src/extensions/default/DefaultExtensions.json b/src/extensions/default/DefaultExtensions.json index 665ade6d0a..15689d606f 100644 --- a/src/extensions/default/DefaultExtensions.json +++ b/src/extensions/default/DefaultExtensions.json @@ -23,6 +23,7 @@ "QuickView", "SVGCodeHints", "UrlCodeHints", + "TypeScriptLanguageServer", "HealthData" ], "desktopOnly": [ diff --git a/src/extensions/default/TypeScriptLanguageServer/client.js b/src/extensions/default/TypeScriptLanguageServer/client.js new file mode 100644 index 0000000000..72226b9374 --- /dev/null +++ b/src/extensions/default/TypeScriptLanguageServer/client.js @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 - present core.ai . 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 }]*/ + +/** + * TypeScript Language Server Client (Node.js side) + * + * ARCHITECTURE NOTE: + * Phoenix runs in the browser, but this file runs in Node.js via Phoenix's NodeDomain. + * Flow: Browser (main.js) → NodeDomain → Node.js (this file) → TypeScript LSP server + * + * This file: + * 1. Runs in a Node.js process spawned by Phoenix's NodeConnection + * 2. Creates a LanguageClient to manage the TypeScript language server + * 3. Spawns the actual typescript-language-server as a child process + * 4. Bridges LSP protocol between Phoenix (browser) and the language server (Node.js) + */ + +var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient, + path = require("path"), + clientName = "TypeScriptLanguageServer", + client = null; + +function getServerOptions() { + // Path to the typescript-language-server executable + // Go from: src/extensions/default/TypeScriptLanguageServer/ to project root + var serverPath = path.resolve(__dirname, "../../../../node_modules/.bin/typescript-language-server"); + + var serverOptions = { + command: serverPath, + args: ["--stdio"] + }; + + return serverOptions; +} + +function setOptions(params) { + var options = { + serverOptions: getServerOptions(), + initializationOptions: { + preferences: { + // Enable all TypeScript/JavaScript features + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + importModuleSpecifierPreference: "relative", + allowIncompleteCompletions: true + } + } + }; + + client.setOptions(options); + + return Promise.resolve("TypeScript Language Server options set successfully"); +} + +function init(domainManager) { + client = new LanguageClient(clientName, domainManager); + client.addOnRequestHandler('setOptions', setOptions); +} + +exports.init = init; diff --git a/src/extensions/default/TypeScriptLanguageServer/main.js b/src/extensions/default/TypeScriptLanguageServer/main.js new file mode 100644 index 0000000000..332a856af6 --- /dev/null +++ b/src/extensions/default/TypeScriptLanguageServer/main.js @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 - present core.ai . All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*eslint max-len: ["error", { "code": 200 }]*/ +define(function (require, exports, module) { + + var LanguageTools = brackets.getModule("languageTools/LanguageTools"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"); + + var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"), + clientName = "TypeScriptLanguageServer", + clientPromise = null, + client = null; + + // Preference to enable/disable TypeScript/JavaScript LSP + PreferencesManager.definePreference("languageTools.enableTypeScriptLSP", "boolean", true, { + description: "Enable TypeScript/JavaScript Language Server Protocol support for enhanced code intelligence" + }); + + function isLSPEnabled() { + return PreferencesManager.get("languageTools.enableTypeScriptLSP"); + } + + AppInit.appReady(function () { + if (!isLSPEnabled()) { + console.log("TypeScript LSP is disabled via preferences"); + return; + } + + console.log("Initializing TypeScript Language Server..."); + + // Initialize the TypeScript/JavaScript language client + // Support both JavaScript and TypeScript files + clientPromise = LanguageTools.initiateToolingService( + clientName, + clientFilePath, + ['javascript', 'typescript', 'jsx', 'tsx'] + ); + + clientPromise.done(function (languageClient) { + client = languageClient; + + // Send custom request to set options + client.sendCustomRequest({ + messageType: "brackets", + type: "setOptions" + }).then(function (response) { + console.log("TypeScript Language Server initialized successfully:", response); + }).catch(function (err) { + console.error("Failed to set TypeScript Language Server options:", err); + }); + + }).fail(function (err) { + console.error("Failed to initialize TypeScript Language Server:", err); + }); + }); + + // Listen for preference changes + PreferencesManager.on("change", "languageTools.enableTypeScriptLSP", function () { + var enabled = isLSPEnabled(); + if (enabled && !client) { + console.log("TypeScript LSP enabled - restart Phoenix to activate"); + } else if (!enabled && client) { + console.log("TypeScript LSP disabled - restart Phoenix to deactivate"); + } + }); + + exports.getClient = function () { + return client; + }; +}); diff --git a/src/extensions/default/TypeScriptLanguageServer/package.json b/src/extensions/default/TypeScriptLanguageServer/package.json new file mode 100644 index 0000000000..04ee88f5b9 --- /dev/null +++ b/src/extensions/default/TypeScriptLanguageServer/package.json @@ -0,0 +1,11 @@ +{ + "name": "phcode-typescript-lsp", + "title": "TypeScript/JavaScript Language Server", + "version": "1.0.0", + "engines": { + "brackets": ">=4.0.0" + }, + "description": "TypeScript/JavaScript Language Server Protocol integration for code intelligence features", + "nodeIsRequired": true, + "dependencies": {} +} diff --git a/src/languageTools/ClientLoader.js b/src/languageTools/ClientLoader.js index e9686970ca..6e9a60a79a 100644 --- a/src/languageTools/ClientLoader.js +++ b/src/languageTools/ClientLoader.js @@ -171,7 +171,7 @@ define(function (require, exports, module) { console.error("ClientInfo domain could not be loaded: ", err); }); } - //initDomainAndHandleNodeCrash(); + initDomainAndHandleNodeCrash(); exports.initiateLanguageClient = initiateLanguageClient; diff --git a/test-lsp.js b/test-lsp.js new file mode 100644 index 0000000000..719b39d281 --- /dev/null +++ b/test-lsp.js @@ -0,0 +1,71 @@ +// Test file for TypeScript LSP +// Open this file in Phoenix to test LSP features + +// Test 1: Code Completion +// Type the dot after 'arr' and press Ctrl+Space +// You should see array methods: map, filter, reduce, etc. +const arr = [1, 2, 3, 4, 5]; +arr + +// Test 2: Hover Information +// Hover your mouse over variables to see their types +const name = "Phoenix"; +const count = 42; +const isActive = true; + +// Test 3: Error Detection +// This should show a red squiggle (undefined variable) +// Uncomment to test: +// const result = undefinedVariable; + +// Test 4: Function Parameter Hints +// Type the opening parenthesis and you should see parameter info +function greet(firstName, lastName, age) { + return `Hello ${firstName} ${lastName}, age ${age}`; +} + +// Type: greet( and you should see parameter hints +greet + +// Test 5: Jump to Definition +// Right-click on 'greet' below and select "Go to Definition" +// It should jump to the function definition above +const message = greet("John", "Doe", 30); + +// Test 6: Object Property Completion +// Type the dot after 'person' and press Ctrl+Space +const person = { + name: "Alice", + age: 25, + email: "alice@example.com" +}; +person + +// Test 7: String Methods +// Type the dot after 'str' and press Ctrl+Space +const str = "Hello World"; +str + +// Test 8: Import Suggestions (if you have other files) +// Type: import { } from './ +// You should see file suggestions + +// Test 9: Type Inference +// Hover over 'numbers' - it should show: number[] +const numbers = [1, 2, 3].map(n => n * 2); + +// Test 10: JSDoc Support +/** + * Calculates the sum of two numbers + * @param {number} a - First number + * @param {number} b - Second number + * @returns {number} The sum + */ +function add(a, b) { + return a + b; +} + +// Type: add( and you should see the JSDoc information +add + +console.log("If you see LSP features working, the integration is successful! 🎉"); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index b9b828dd66..f841ec5471 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -143,6 +143,6 @@ define(function (require, exports, module) { // Node Tests require("spec/NodeConnection-test"); // todo TEST_MODERN - // require("spec/LanguageTools-test"); LSP tests. disabled for now + require("spec/LanguageTools-test"); // require("spec/Menu-native-integ-test"); evaluate after we have native menus in os installed builds });