Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
4 changes: 3 additions & 1 deletion docs/01-about/020_programming-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
185 changes: 185 additions & 0 deletions src/solidlsp/language_servers/tsgo_language_server.py
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 10 additions & 1 deletion src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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", ""]:
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand Down Expand Up @@ -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],
}


Expand Down
13 changes: 10 additions & 3 deletions test/solidlsp/typescript/test_typescript_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 = []
Expand Down
Loading