diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 86246a2e4..8438fa7a6 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -399,6 +399,10 @@ jobs: uses: krdlab/setup-haxe@v2 with: haxe-version: 4.3.7 + - name: Install tsgo (TypeScript native LSP) + shell: bash + run: npm install -g @typescript/native-preview + - name: Install Elm shell: bash run: npm install -g elm@0.19.1-6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6747ece89..462881f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356 - Add JSON language server support via `vscode-json-languageserver` (experimental) #1391 - Fix: Elixir/Expert deadlock on startup — Expert's build pipeline requires a `textDocument/didOpen` notification to start; Serena now opens `mix.exs` immediately after `initialized` so Expert begins compiling instead of waiting indefinitely #1397 + - New: Add `typescript_tsgo` language backend — TypeScript support via tsgo (native Go-based TypeScript 7 compiler with built-in LSP); automatically installed via npm #1402 * Dashboard: - Add configurable dashboard interface mode (new global configuration setting `web_dashboard_interface`): diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index dff0a228b..56ff85391 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -112,7 +112,9 @@ Some languages require additional installations or setup steps, as noted. works best with a `foundry.toml` or `hardhat.config.js` in the project root) * **Swift** * **TypeScript** -* **Vue** + (by default, uses [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) (language `typescript`); + alternatively supports [tsgo](https://github.com/nicolo-ribaudo/tc39-proposal-type-annotations) (language `typescript_tsgo`), the native Go-based TypeScript 7 compiler with built-in LSP — automatically installed via npm) +* **Vue** (3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection) * **YAML** * **JSON** diff --git a/pyproject.toml b/pyproject.toml index 35c00d2d3..b528962ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -349,6 +349,7 @@ markers = [ "solidity: language server running for Solidity (uses @nomicfoundation/solidity-language-server)", "ansible: language server running for Ansible (uses @ansible/ansible-language-server)", "msl: language server running for mSL (mIRC Scripting Language)", + "typescript_tsgo: language server running for TypeScript via tsgo (native Go-based TS7 compiler)", ] [tool.codespell] diff --git a/src/solidlsp/language_servers/tsgo_language_server.py b/src/solidlsp/language_servers/tsgo_language_server.py new file mode 100644 index 000000000..8a3a133a1 --- /dev/null +++ b/src/solidlsp/language_servers/tsgo_language_server.py @@ -0,0 +1,185 @@ +""" +Language server implementation for tsgo — TypeScript 7's native Go-based compiler with built-in LSP. + +tsgo (part of @typescript/native-preview) implements the Language Server Protocol natively, +without requiring Node.js or the traditional typescript-language-server wrapper. + +Launch command: tsgo --lsp --stdio +""" + +import logging +import os +import pathlib +import shutil +from typing import cast + +from overrides import override +from sensai.util.logging import LogTime + +from solidlsp.ls import LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.settings import SolidLSPSettings + +from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command +from .typescript_language_server import prefer_non_node_modules_definition + +log = logging.getLogger(__name__) + + +class TsgoLanguageServer(SolidLanguageServer): + """ + TypeScript support via tsgo — the native Go-based TypeScript 7 compiler with built-in LSP. + + tsgo is installed automatically via npm (``@typescript/native-preview``). + It does not require the traditional typescript-language-server wrapper. + + You can pass the following entries in ls_specific_settings["typescript_tsgo"]: + - tsgo_version: Version of @typescript/native-preview to install (default: "7.0.0-dev.20250601") + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [ + "node_modules", + "dist", + "build", + "coverage", + ".next", + "out", + ] + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """Install @typescript/native-preview via npm and return the path to the tsgo executable.""" + language_specific_config = self._custom_settings + tsgo_version = language_specific_config.get("tsgo_version", "7.0.0-dev.20250601") + npm_registry = language_specific_config.get("npm_registry") + + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="typescript-native-preview", + description="@typescript/native-preview (tsgo) package", + command=build_npm_install_command("@typescript/native-preview", tsgo_version, npm_registry), + platform_id="any", + ), + ] + ) + + # Verify npm is installed + is_npm_installed = shutil.which("npm") is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + tsgo_ls_dir = os.path.join(self._ls_resources_dir, "tsgo-lsp") + tsgo_executable_path = os.path.join(tsgo_ls_dir, "node_modules", ".bin", "tsgo") + + # Check if installation is needed + version_file = os.path.join(tsgo_ls_dir, ".installed_version") + expected_version = tsgo_version + + needs_install = False + if not os.path.exists(tsgo_executable_path): + log.info(f"tsgo executable not found at {tsgo_executable_path}.") + needs_install = True + elif os.path.exists(version_file): + with open(version_file) as f: + installed_version = f.read().strip() + if installed_version != expected_version: + log.info(f"tsgo version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling...") + needs_install = True + else: + log.info("tsgo version file not found. Reinstalling to ensure correct version...") + needs_install = True + + if needs_install: + log.info("Installing tsgo dependencies...") + with LogTime("Installation of tsgo (typescript/native-preview)", logger=log): + deps.install(tsgo_ls_dir) + with open(version_file, "w") as f: + f.write(expected_version) + log.info("tsgo installed successfully") + + if not os.path.exists(tsgo_executable_path): + raise FileNotFoundError(f"tsgo executable not found at {tsgo_executable_path}, something went wrong with the installation.") + return tsgo_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "--lsp", "--stdio"] + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + super().__init__( + config, + repository_root_path, + None, + "typescript", + solidlsp_settings, + ) + + def _create_dependency_provider(self) -> "TsgoLanguageServer.DependencyProvider": + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params: dict = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "implementation": {"dynamicRegistration": True}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return cast(InitializeParams, initialize_params) + + def _start_server(self) -> None: + def register_capability_handler(params: dict) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + + def do_nothing(params: dict) -> None: + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + log.info("Starting tsgo language server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request to tsgo LSP") + init_response = self.server.send.initialize(initialize_params) + + assert "definitionProvider" in init_response.get("capabilities", {}), "tsgo did not report definitionProvider capability" + + self.server.notify.initialized({}) + + @override + def _get_preferred_definition(self, definitions: list) -> dict: + return prefer_non_node_modules_definition(definitions) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index eb527dc34..d7e1e4391 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -95,6 +95,10 @@ class Language(str, Enum): a virtual environment with pygls dependencies on first use. """ # Experimental or deprecated Language Servers + TYPESCRIPT_TSGO = "typescript_tsgo" + """TypeScript support via tsgo — the native Go-based TypeScript 7 compiler with built-in LSP. + Does not require Node.js. Install via: npm install -g @typescript/native-preview + """ TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" PYTHON_JEDI = "python_jedi" @@ -172,6 +176,7 @@ def is_experimental(self) -> bool: """ return self in { self.ANSIBLE, + self.TYPESCRIPT_TSGO, self.TYPESCRIPT_VTS, self.PYTHON_JEDI, self.PYTHON_TY, @@ -213,7 +218,7 @@ def get_source_fn_matcher(self) -> FilenameMatcher: return FilenameMatcher("*.py", "*.pyi") case self.JAVA: return FilenameMatcher("*.java") - case self.TYPESCRIPT | self.TYPESCRIPT_VTS: + case self.TYPESCRIPT | self.TYPESCRIPT_TSGO | self.TYPESCRIPT_VTS: # see https://github.com/oraios/serena/issues/204 path_patterns = [] for prefix in ["c", "m", ""]: @@ -383,6 +388,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]: from solidlsp.language_servers.vts_language_server import VtsLanguageServer return VtsLanguageServer + case self.TYPESCRIPT_TSGO: + from solidlsp.language_servers.tsgo_language_server import TsgoLanguageServer + + return TsgoLanguageServer case self.VUE: from solidlsp.language_servers.vue_language_server import VueLanguageServer diff --git a/test/conftest.py b/test/conftest.py index 4d3f1154d..8c31d6684 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -43,6 +43,7 @@ class LanguageParamRequest: Language.PYTHON_JEDI: Language.PYTHON, Language.RUBY_SOLARGRAPH: Language.RUBY, Language.PYTHON_TY: Language.PYTHON, + Language.TYPESCRIPT_TSGO: Language.TYPESCRIPT, } @@ -268,6 +269,7 @@ def project_with_ls(request: LanguageParamRequest) -> Iterator[Project]: Language.PYTHON_TY: [pytest.mark.python], Language.RUST: [pytest.mark.rust], Language.TYPESCRIPT: [pytest.mark.typescript], + Language.TYPESCRIPT_TSGO: [pytest.mark.typescript], } diff --git a/test/solidlsp/typescript/test_typescript_basic.py b/test/solidlsp/typescript/test_typescript_basic.py index ba86ba042..5c4585fa3 100644 --- a/test/solidlsp/typescript/test_typescript_basic.py +++ b/test/solidlsp/typescript/test_typescript_basic.py @@ -5,19 +5,26 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci, language_tests_enabled from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +_typescript_servers: list[Language] = [Language.TYPESCRIPT] + +# tsgo is always tested in CI; locally only if the language is enabled +if is_ci or language_tests_enabled(Language.TYPESCRIPT_TSGO): + _typescript_servers.append(Language.TYPESCRIPT_TSGO) + @pytest.mark.typescript class TestTypescriptLanguageServer: - @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + @pytest.mark.parametrize("language_server", _typescript_servers, indirect=True) def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "printValue"), "printValue method not found in symbol tree" - @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + @pytest.mark.parametrize("language_server", _typescript_servers, indirect=True) def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("index.ts") symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() @@ -33,7 +40,7 @@ def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> "index.ts should reference helperFunction (tried all positions in selectionRange)" ) - @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + @pytest.mark.parametrize("language_server", _typescript_servers, indirect=True) def test_bare_symbol_names(self, language_server) -> None: all_symbols = request_all_symbols(language_server) malformed_symbols = []