Merge pull request #933 from oraios/ls-dependency-provider

Introduce LanguageServerDependencyProvider abstraction
This commit is contained in:
Dominik Jain authored and GitHub committed 2026-01-15 21:36:07 +01:00
commit b0dd8512bb
13 files changed
+1141 -1010

No files matched your search

@@ -10,72 +10,48 @@ Adding a new language involves:
2. **Language Registration** - Adding the language to enums and configurations
3. **Test Repository** - Creating a minimal test project
4. **Test Suite** - Writing comprehensive tests
5. **Runtime Dependencies** - Configuring automatic language server downloads
## Step 1: Language Server Implementation
### 1.1 Create Language Server Class
Create a new file in `src/solidlsp/language_servers/` (e.g., `new_language_server.py`).
Have a look at `intelephense.py` for a reference implementation of a language server which downloads all its dependencies, at `gopls.py` for an LS that needs some preinstalled
dependencies, and on `pyright_server.py` that does not need any additional dependencies
because the language server can be installed directly as python package.
```python
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
#### Providing the Launch Command via a DependencyProvider
All language servers use the `DependencyProvider` pattern to handle
* runtime dependency installation/discovery
* launch command creation (and, optionally, environment setup)
class NewLanguageServer(SolidLanguageServer):
"""
Language server implementation for NewLanguage.
"""
To implement a new language server using the DependencyProvider pattern:
* Pass `None` for `process_launch_info` in `super().__init__()` - the base class creates it via `_create_dependency_provider()`
* Implement `_create_dependency_provider()` to return an inner `DependencyProvider` class instance.
In simple cases, it can be instantiated with only two parameters:
```python
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
```
The resource dir that is passed is the directory in which installed dependencies should be stored!
def __init__(self, config: LanguageServerConfig, repository_root_path: str):
# Determine language server command
cmd = self._get_language_server_command()
**Base Classes:**
super().__init__(config,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"new_language",,
- **`LanguageServerDependencyProviderSinglePath`** - For language servers with a single core dependency (e.g., an executable or JAR file)
- Provides automatic support for the `ls_path` custom setting, allowing users to override the core dependency path (if they have it installed it themselves)
- Implement `_get_or_install_core_dependency()` to return the path to the core dependency, downloading/installing it automatically if necessary
- Implement `_create_launch_command(core_path)` to build the full command from the core path
- Reference implementations: `TypeScriptLanguageServer`, `Intelephense`, `ClojureLSP`, `ClangdLanguageServer`, `PyrightServer`
def _get_language_server_command(self) -> list[str]:
"""Get the command to start the language server."""
# Example: return ["new-language-server", "--stdio"]
pass
- **`LanguageServerDependencyProvider`** - The base class, which can be directly inherited from for complex cases with multiple dependencies or custom setup
- Implement `create_launch_command()` directly
- Reference implementations: `EclipseJDTLS`, `CSharpLanguageServer`, `MatlabLanguageServer`
@override
def is_ignored_dirname(self, dirname: str) -> bool:
"""Define language-specific directories to ignore."""
return super().is_ignored_dirname(dirname) or dirname in ["build",
"dist", "target"]
```
**Implementation Pointers::**
- When returning the command, prefer the list-based representation for robustness
- Override `create_launch_command_env` if the launch command needs environment variables to be set (defaults to `{}` in the base implementation)
### 1.2 Language Server Discovery and Installation
You should look at at least one existing implementation of each base class to understand how they work.
For languages requiring automatic installation, implement download logic similar to C#:
```python
@classmethod
def _ensure_server_installed(cls) -> str:
"""Ensure language server is installed and return path."""
# Check system installation first
system_server = shutil.which("new-language-server")
if system_server:
return system_server
# Download and install if needed
server_path = cls._download_and_install_server()
return server_path
def _download_and_install_server(cls) -> str:
"""Download and install the language server."""
# Implementation specific to your language server
pass
```
### 1.3 LSP Initialization
### 1.2 LSP Initialization
Override initialization methods if needed:
@@ -98,9 +74,14 @@ def _start_server(self):
# Start server and initialize
self.server.start()
init_response = self.server.send.initialize(self._get_initialize_params())
self.server.notify.initialized({})
```
After `_start_server` returns, the language server should be fully operational.
If the server requires that one waits for certain notifications or responses before being ready, implement that logic here.
For an example, see `EclipseJDTLS._start_server`.
## Step 2: Language Registration
### 2.1 Add to Language Enum
@@ -215,6 +196,6 @@ Consider adding new cases to the parametrized tests in `test_serena_agent.py` fo
Update:
- **README.md** - Add language to supported languages list
- **README.md** - Add language to the list of languages
- **docs/01-about/020_programming-languages.md** - Add language to the list and mention any special notes, compatibility or requirements (e.g. installations the user is required to do)
- **CHANGELOG.md** - Document the new language support
- **Language-specific docs** - Installation requirements, known issues
+25 -6
View File
@@ -134,9 +134,10 @@ For advanced users, Serena's configuration can be further customized.
The Serena user data directory (where configuration, language server files, logs, etc. are stored) defaults to `~/.serena`.
You can change this location by setting the `SERENA_HOME` environment variable to your desired path.
### Language server specific settings (`ls_specific_settings`)
### Language Server-Specific settings (`ls_specific_settings`)
`ls_specific_settings` lets you pass per-language, language-server-specific configuration.
Under the key `ls_specific_settings` in `serena_config.yml`, you can you pass per-language,
language server-specific configuration.
Structure:
@@ -146,10 +147,28 @@ ls_specific_settings:
# language-server-specific keys
```
Where to put it:
- Global default: `~/.serena/serena_config.yml`
- Per-project override: `<project>/.serena/project.yml` (takes precedence over the global config for the same keys)
- There is currently no `serena-mcp-server` CLI option to set `ls_specific_settings`; use the config files above.
:::{attention}
Most settings are currently undocumented. Please refer to the
[source code of the respective language server](https://github.com/oraios/serena/tree/main/src/solidlsp/language_servers)
implementation to determine supported settings.
:::
#### Overriding the Language Server Path
Some language servers, particularly those that use a single core path for the language server (e.g. the main executable),
support overriding that path via the `ls_path` setting.
Therefore, if you have installed the language server yourself and want to use your installation
instead of Serena's managed installation, you can set the `ls_path` setting as follows:
```yaml
ls_specific_settings:
<language>:
ls_path: "/path/to/language-server"
```
This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`.
Currently, this includes the following languages: `clojure`, `cpp`, `php`, `python`, `typescript`.
We will add support for more languages over time.
#### Go (`gopls`)
@@ -8,10 +8,9 @@ import pathlib
import threading
from typing import Any, cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -30,92 +29,95 @@ class ClangdLanguageServer(SolidLanguageServer):
"""
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clangd_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", solidlsp_settings
)
super().__init__(config, repository_root_path, None, "cpp", solidlsp_settings)
self.server_ready = threading.Event()
self.service_ready_event = threading.Event()
self.initialize_searcher_command_available = threading.Event()
self.resolve_main_method_available = threading.Event()
@classmethod
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
"""
import shutil
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="Clangd",
description="Clangd for Linux (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
RuntimeDependency(
id="Clangd",
description="Clangd for Windows (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd.exe",
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (Arm64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
]
)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Setup runtime dependencies for ClangdLanguageServer and return the path to the executable.
"""
import shutil
clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="Clangd",
description="Clangd for Linux (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
RuntimeDependency(
id="Clangd",
description="Clangd for Windows (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd.exe",
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (Arm64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
),
]
)
try:
dep = deps.get_single_dep_for_current_platform()
except RuntimeError:
dep = None
clangd_ls_dir = os.path.join(self._ls_resources_dir, "clangd")
if dep is None:
# No prebuilt binary available, look for system-installed clangd
clangd_executable_path = shutil.which("clangd")
if not clangd_executable_path:
raise FileNotFoundError(
"Clangd is not installed on your system.\n"
+ "Please install clangd using your system package manager:\n"
+ " Ubuntu/Debian: sudo apt-get install clangd\n"
+ " Fedora/RHEL: sudo dnf install clang-tools-extra\n"
+ " Arch Linux: sudo pacman -S clang\n"
+ "See https://clangd.llvm.org/installation for more details."
)
log.info(f"Using system-installed clangd at {clangd_executable_path}")
else:
# Standard download and install for platforms with prebuilt binaries
clangd_executable_path = deps.binary_path(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}")
_ = deps.install(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
raise FileNotFoundError(
f"Clangd executable not found at {clangd_executable_path}.\n"
+ "Make sure you have installed clangd. See https://clangd.llvm.org/installation"
)
os.chmod(clangd_executable_path, 0o755)
return clangd_executable_path
try:
dep = deps.get_single_dep_for_current_platform()
except RuntimeError:
dep = None
if dep is None:
# No prebuilt binary available, look for system-installed clangd
clangd_executable_path = shutil.which("clangd")
if not clangd_executable_path:
raise FileNotFoundError(
"Clangd is not installed on your system.\n"
+ "Please install clangd using your system package manager:\n"
+ " Ubuntu/Debian: sudo apt-get install clangd\n"
+ " Fedora/RHEL: sudo dnf install clang-tools-extra\n"
+ " Arch Linux: sudo pacman -S clang\n"
+ "See https://clangd.llvm.org/installation for more details."
)
log.info(f"Using system-installed clangd at {clangd_executable_path}")
else:
# Standard download and install for platforms with prebuilt binaries
clangd_executable_path = deps.binary_path(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}")
_ = deps.install(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
raise FileNotFoundError(
f"Clangd executable not found at {clangd_executable_path}.\n"
+ "Make sure you have installed clangd. See https://clangd.llvm.org/installation"
)
os.chmod(clangd_executable_path, 0o755)
return clangd_executable_path
def _create_launch_command(self, core_path: str) -> list[str] | str:
return [core_path]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
+24 -21
View File
@@ -10,10 +10,9 @@ import subprocess
import threading
from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -91,11 +90,10 @@ class ClojureLSP(SolidLanguageServer):
"""
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clojure_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
None,
"clojure",
solidlsp_settings,
)
@@ -104,24 +102,29 @@ class ClojureLSP(SolidLanguageServer):
self.resolve_main_method_available = threading.Event()
self.service_ready_event = threading.Event()
@classmethod
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""Setup runtime dependencies for clojure-lsp and return the command to start the server."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
dependency = deps.get_single_dep_for_current_platform()
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
log.info(
f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
)
deps.install(clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
os.chmod(clojurelsp_executable_path, 0o755)
return clojurelsp_executable_path
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""Setup runtime dependencies for clojure-lsp and return the path to the executable."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
dependency = deps.get_single_dep_for_current_platform()
clojurelsp_executable_path = deps.binary_path(self._ls_resources_dir)
if not os.path.exists(clojurelsp_executable_path):
log.info(
f"Downloading and extracting clojure-lsp from {dependency.url} to {self._ls_resources_dir}",
)
deps.install(self._ls_resources_dir)
if not os.path.exists(clojurelsp_executable_path):
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
os.chmod(clojurelsp_executable_path, 0o755)
return clojurelsp_executable_path
def _create_launch_command(self, core_path: str) -> list[str] | str:
return [core_path]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
@@ -18,12 +18,11 @@ from typing import Any, cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.zip import SafeZipExtractor
@@ -212,266 +211,276 @@ class CSharpLanguageServer(SolidLanguageServer):
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
dotnet_path, language_server_path = self._ensure_server_installed(config, solidlsp_settings)
# Find solution or project file
solution_or_project = find_solution_or_project_file(repository_root_path)
# Create log directory
log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
# Build command using dotnet directly
cmd = [dotnet_path, language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"]
# The language server will discover the solution/project from the workspace root
if solution_or_project:
log.info(f"Found solution/project file: {solution_or_project}")
else:
log.warning("No .sln or .csproj file found, language server will attempt auto-discovery")
log.debug(f"Language server command: {' '.join(cmd)}")
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings)
super().__init__(config, repository_root_path, None, "csharp", solidlsp_settings)
self.initialization_complete = threading.Event()
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
@classmethod
def _ensure_server_installed(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[str, str]:
"""
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path).
"""
language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", []))
class DependencyProvider(LanguageServerDependencyProvider):
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
ls_resources_dir: str,
solidlsp_settings: SolidLSPSettings,
repository_root_path: str,
):
super().__init__(custom_settings, ls_resources_dir)
self._solidlsp_settings = solidlsp_settings
self._repository_root_path = repository_root_path
self._dotnet_path, self._language_server_path = self._ensure_server_installed()
log.debug("Resolving runtime dependencies")
def create_launch_command(self) -> list[str] | str:
# Find solution or project file
solution_or_project = find_solution_or_project_file(self._repository_root_path)
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
overrides=runtime_dependency_overrides,
)
# Create log directory
log_dir = Path(self._ls_resources_dir) / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log.debug(
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
)
# Build command using dotnet directly
cmd = [self._dotnet_path, self._language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"]
# Find the dependencies for our platform
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(dotnet_runtime_dep, solidlsp_settings)
server_dll_path = CSharpLanguageServer._ensure_language_server(lang_server_dep, solidlsp_settings)
# The language server will discover the solution/project from the workspace root
if solution_or_project:
log.info(f"Found solution/project file: {solution_or_project}")
else:
log.warning("No .sln or .csproj file found, language server will attempt auto-discovery")
return dotnet_path, server_dll_path
log.debug(f"Language server command: {' '.join(cmd)}")
@classmethod
def _ensure_dotnet_runtime(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
return cmd
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
def _ensure_server_installed(self) -> tuple[str, str]:
"""
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path).
"""
runtime_dependency_overrides = cast(list[dict[str, Any]], self._custom_settings.get("runtime_dependencies", []))
# Download .NET 9 runtime using config
return cls._ensure_dotnet_runtime_from_config(dotnet_runtime_dep, solidlsp_settings)
log.debug("Resolving runtime dependencies")
@classmethod
def _ensure_language_server(cls, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""Ensure language server is available and return the DLL path."""
package_name = lang_server_dep.package_name
package_version = lang_server_dep.package_version
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
overrides=runtime_dependency_overrides,
)
server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}"
assert lang_server_dep.binary_name is not None
server_dll = server_dir / lang_server_dep.binary_name
log.debug(
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
)
if server_dll.exists():
log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
# Find the dependencies for our platform
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
dotnet_path = self._ensure_dotnet_runtime(dotnet_runtime_dep)
server_dll_path = self._ensure_language_server(lang_server_dep)
return dotnet_path, server_dll_path
def _ensure_dotnet_runtime(self, dotnet_runtime_dep: RuntimeDependency) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
# Download .NET 9 runtime using config
return self._ensure_dotnet_runtime_from_config(dotnet_runtime_dep)
def _ensure_language_server(self, lang_server_dep: RuntimeDependency) -> str:
"""Ensure language server is available and return the DLL path."""
package_name = lang_server_dep.package_name
package_version = lang_server_dep.package_version
server_dir = Path(self._ls_resources_dir) / f"{package_name}.{package_version}"
assert lang_server_dep.binary_name is not None
server_dll = server_dir / lang_server_dep.binary_name
if server_dll.exists():
log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
return str(server_dll)
# Download and install the language server
log.info(f"Downloading {package_name} version {package_version}...")
assert package_version is not None
assert package_name is not None
package_path = self._download_nuget_package_direct(package_name, package_version)
# Extract and install
self._extract_language_server(lang_server_dep, package_path, server_dir)
if not server_dll.exists():
raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
# Make executable on Unix systems
if platform.system().lower() != "windows":
server_dll.chmod(0o755)
log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
return str(server_dll)
# Download and install the language server
log.info(f"Downloading {package_name} version {package_version}...")
assert package_version is not None
assert package_name is not None
package_path = cls._download_nuget_package_direct(package_name, package_version, solidlsp_settings)
@staticmethod
def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
"""Extract language server files from downloaded package."""
extract_path = lang_server_dep.extract_path or "lib/net9.0"
source_dir = package_path / extract_path
# Extract and install
cls._extract_language_server(lang_server_dep, package_path, server_dir)
if not source_dir.exists():
# Try alternative locations
for possible_dir in [
package_path / "tools" / "net9.0" / "any",
package_path / "lib" / "net9.0",
package_path / "contentFiles" / "any" / "net9.0",
]:
if possible_dir.exists():
source_dir = possible_dir
break
else:
raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}")
if not server_dll.exists():
raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
# Copy files to cache directory
server_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
# Make executable on Unix systems
if platform.system().lower() != "windows":
server_dll.chmod(0o755)
def _download_nuget_package_direct(self, package_name: str, package_version: str) -> Path:
"""
Download a NuGet package directly from the Azure NuGet feed.
Returns the path to the extracted package directory.
"""
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
return str(server_dll)
# Create temporary directory for package download
temp_dir = Path(self._ls_resources_dir) / "temp_downloads"
temp_dir.mkdir(parents=True, exist_ok=True)
@staticmethod
def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
"""Extract language server files from downloaded package."""
extract_path = lang_server_dep.extract_path or "lib/net9.0"
source_dir = package_path / extract_path
if not source_dir.exists():
# Try alternative locations
for possible_dir in [
package_path / "tools" / "net9.0" / "any",
package_path / "lib" / "net9.0",
package_path / "contentFiles" / "any" / "net9.0",
]:
if possible_dir.exists():
source_dir = possible_dir
break
else:
raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}")
# Copy files to cache directory
server_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
@classmethod
def _download_nuget_package_direct(cls, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings) -> Path:
"""
Download a NuGet package directly from the Azure NuGet feed.
Returns the path to the extracted package directory.
"""
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
# Create temporary directory for package download
temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads"
temp_dir.mkdir(parents=True, exist_ok=True)
try:
# First, get the service index from the Azure feed
log.debug("Fetching NuGet service index from Azure feed...")
with urllib.request.urlopen(azure_feed_url) as response:
service_index = json.loads(response.read().decode())
# Find the package base address (for downloading packages)
package_base_address = None
for resource in service_index.get("resources", []):
if resource.get("@type") == "PackageBaseAddress/3.0.0":
package_base_address = resource.get("@id")
break
if not package_base_address:
raise SolidLSPException("Could not find package base address in Azure NuGet feed")
# Construct the download URL for the specific package
package_id_lower = package_name.lower()
package_version_lower = package_version.lower()
package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"
log.debug(f"Downloading package from: {package_url}")
# Download the .nupkg file
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
urllib.request.urlretrieve(package_url, nupkg_file)
# Extract the .nupkg file (it's just a zip file)
package_extract_dir = temp_dir / f"{package_name}.{package_version}"
package_extract_dir.mkdir(exist_ok=True)
# Use SafeZipExtractor to handle long paths and skip errors
extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False)
extractor.extract_all()
# Clean up the nupkg file
nupkg_file.unlink()
log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
return package_extract_dir
except Exception as e:
raise SolidLSPException(
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
) from e
@classmethod
def _ensure_dotnet_runtime_from_config(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""
Ensure .NET 9 runtime is available using runtime dependency configuration.
Returns the path to the dotnet executable.
"""
# TODO: use RuntimeDependency util methods instead of custom download logic
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
# First, get the service index from the Azure feed
log.debug("Fetching NuGet service index from Azure feed...")
with urllib.request.urlopen(azure_feed_url) as response:
service_index = json.loads(response.read().decode())
# Download .NET 9 runtime using config
dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0"
assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
# Find the package base address (for downloading packages)
package_base_address = None
for resource in service_index.get("resources", []):
if resource.get("@type") == "PackageBaseAddress/3.0.0":
package_base_address = resource.get("@id")
break
if dotnet_exe.exists():
log.info(f"Using cached .NET runtime from {dotnet_exe}")
return str(dotnet_exe)
if not package_base_address:
raise SolidLSPException("Could not find package base address in Azure NuGet feed")
# Download .NET runtime
log.info("Downloading .NET 9 runtime...")
dotnet_dir.mkdir(parents=True, exist_ok=True)
# Construct the download URL for the specific package
package_id_lower = package_name.lower()
package_version_lower = package_version.lower()
package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"
custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
custom_dotnet_runtime_url = custom_settings.get("dotnet_runtime_url")
if custom_dotnet_runtime_url is not None:
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
url = custom_dotnet_runtime_url
else:
url = dotnet_runtime_dep.url
log.debug(f"Downloading package from: {package_url}")
archive_type = dotnet_runtime_dep.archive_type
# Download the .nupkg file
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
urllib.request.urlretrieve(package_url, nupkg_file)
# Download the runtime
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
try:
log.debug(f"Downloading from {url}")
urllib.request.urlretrieve(url, download_path)
# Extract the .nupkg file (it's just a zip file)
package_extract_dir = temp_dir / f"{package_name}.{package_version}"
package_extract_dir.mkdir(exist_ok=True)
# Extract the archive
if archive_type == "zip":
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(dotnet_dir)
# Use SafeZipExtractor to handle long paths and skip errors
extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False)
extractor.extract_all()
# Clean up the nupkg file
nupkg_file.unlink()
log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
return package_extract_dir
except Exception as e:
raise SolidLSPException(
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
) from e
def _ensure_dotnet_runtime_from_config(self, dotnet_runtime_dep: RuntimeDependency) -> str:
"""
Ensure .NET 9 runtime is available using runtime dependency configuration.
Returns the path to the dotnet executable.
"""
# TODO: use RuntimeDependency util methods instead of custom download logic
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
if system_dotnet:
# Check if it's .NET 9
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
# Download .NET 9 runtime using config
dotnet_dir = Path(self._ls_resources_dir) / "dotnet-runtime-9.0"
assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
if dotnet_exe.exists():
log.info(f"Using cached .NET runtime from {dotnet_exe}")
return str(dotnet_exe)
# Download .NET runtime
log.info("Downloading .NET 9 runtime...")
dotnet_dir.mkdir(parents=True, exist_ok=True)
custom_dotnet_runtime_url = self._custom_settings.get("dotnet_runtime_url")
if custom_dotnet_runtime_url is not None:
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
url = custom_dotnet_runtime_url
else:
# tar.gz
with tarfile.open(download_path, "r:gz") as tar_ref:
tar_ref.extractall(dotnet_dir)
url = dotnet_runtime_dep.url
# Remove the archive
download_path.unlink()
archive_type = dotnet_runtime_dep.archive_type
# Make dotnet executable on Unix
if platform.system().lower() != "windows":
dotnet_exe.chmod(0o755)
# Download the runtime
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
try:
log.debug(f"Downloading from {url}")
urllib.request.urlretrieve(url, download_path)
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
return str(dotnet_exe)
# Extract the archive
if archive_type == "zip":
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(dotnet_dir)
else:
# tar.gz
with tarfile.open(download_path, "r:gz") as tar_ref:
tar_ref.extractall(dotnet_dir)
except Exception as e:
raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
# Remove the archive
download_path.unlink()
# Make dotnet executable on Unix
if platform.system().lower() != "windows":
dotnet_exe.chmod(0o755)
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
return str(dotnet_exe)
except Exception as e:
raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
def _get_initialize_params(self) -> InitializeParams:
"""
+246 -233
View File
@@ -16,12 +16,11 @@ from typing import cast
from overrides import override
from solidlsp import ls_types
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_types import UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -69,90 +68,19 @@ class EclipseJDTLS(SolidLanguageServer):
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self._setupRuntimeDependencies(config, solidlsp_settings)
self.runtime_dependency_paths = runtime_dependency_paths
super().__init__(config, repository_root_path, None, "java", solidlsp_settings)
# ws_dir is the workspace directory for the EclipseJDTLS server
ws_dir = str(
PurePath(
solidlsp_settings.ls_resources_dir,
"EclipseJDTLS",
"workspaces",
uuid.uuid4().hex,
)
)
# shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex"))
os.makedirs(shared_cache_location, exist_ok=True)
os.makedirs(ws_dir, exist_ok=True)
jre_path = self.runtime_dependency_paths.jre_path
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
data_dir = str(PurePath(ws_dir, "data_dir"))
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path
if not os.path.exists(jdtls_config_path):
shutil.copytree(jdtls_readonly_config_path, jdtls_config_path)
for static_path in [
jre_path,
lombok_jar_path,
jdtls_launcher_jar,
jdtls_config_path,
jdtls_readonly_config_path,
]:
assert os.path.exists(static_path), static_path
# TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well
proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path}
proc_cwd = repository_root_path
cmd = [
jre_path,
"--add-modules=ALL-SYSTEM",
"--add-opens",
"java.base/java.util=ALL-UNNAMED",
"--add-opens",
"java.base/java.lang=ALL-UNNAMED",
"--add-opens",
"java.base/sun.nio.fs=ALL-UNNAMED",
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
"-Dosgi.bundles.defaultStartLevel=4",
"-Declipse.product=org.eclipse.jdt.ls.core.product",
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
"-Dfile.encoding=utf8",
"-noverify",
"-XX:+UseParallelGC",
"-XX:GCTimeRatio=4",
"-XX:AdaptiveSizePolicyWeight=90",
"-Dsun.zip.disableMemoryMapping=true",
"-Djava.lsp.joinOnCompletion=true",
"-Xmx3G",
"-Xms100m",
"-Xlog:disable",
"-Dlog.level=ALL",
f"-javaagent:{lombok_jar_path}",
f"-Djdt.core.sharedIndexLocation={shared_cache_location}",
"-jar",
f"{jdtls_launcher_jar}",
"-configuration",
f"{jdtls_config_path}",
"-data",
f"{data_dir}",
]
# Extract runtime_dependency_paths from the dependency provider
assert isinstance(self._dependency_provider, self.DependencyProvider)
self.runtime_dependency_paths = self._dependency_provider.runtime_dependency_paths
self._service_ready_event = threading.Event()
self._project_ready_event = threading.Event()
self._intellicode_enable_command_available = threading.Event()
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
ls_resources_dir = self.ls_resources_dir(self._solidlsp_settings)
return self.DependencyProvider(self._custom_settings, ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
@@ -172,168 +100,253 @@ class EclipseJDTLS(SolidLanguageServer):
"lib", # General
]
@classmethod
def _setupRuntimeDependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
"""
platformId = PlatformUtils.get_platform_id()
class DependencyProvider(LanguageServerDependencyProvider):
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
ls_resources_dir: str,
solidlsp_settings: SolidLSPSettings,
repository_root_path: str,
):
super().__init__(custom_settings, ls_resources_dir)
self._solidlsp_settings = solidlsp_settings
self._repository_root_path = repository_root_path
self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir)
runtime_dependencies = {
"gradle": {
"platform-agnostic": {
"url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip",
"archiveType": "zip",
"relative_extraction_path": ".",
}
},
"vscode-java": {
"darwin-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
},
"osx-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-macosx-aarch64",
"jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_mac_arm",
},
"osx-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-macosx-x86_64",
"jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_mac",
},
"linux-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-linux-aarch64",
"jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_linux_arm",
},
"linux-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-linux-x86_64",
"jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_linux",
},
"win-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-win32-x86_64",
"jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_win",
},
},
"intellicode": {
"platform-agnostic": {
"url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
"alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage",
"archiveType": "zip",
"relative_extraction_path": "intellicode",
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
}
},
}
@classmethod
def _setup_runtime_dependencies(cls, ls_resources_dir: str) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
"""
platformId = PlatformUtils.get_platform_id()
# assert platformId.value in [
# "linux-x64",
# "win-x64",
# ], "Only linux-x64 platform is supported for in multilspy at the moment"
runtime_dependencies = {
"gradle": {
"platform-agnostic": {
"url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip",
"archiveType": "zip",
"relative_extraction_path": ".",
}
},
"vscode-java": {
"darwin-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
},
"osx-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-macosx-aarch64",
"jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_mac_arm",
},
"osx-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-macosx-x86_64",
"jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_mac",
},
"linux-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-linux-aarch64",
"jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_linux_arm",
},
"linux-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-linux-x86_64",
"jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_linux",
},
"win-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/21.0.7-win32-x86_64",
"jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
"jdtls_readonly_config_path": "extension/server/config_win",
},
},
"intellicode": {
"platform-agnostic": {
"url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
"alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage",
"archiveType": "zip",
"relative_extraction_path": "intellicode",
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
}
},
}
gradle_path = str(
PurePath(
cls.ls_resources_dir(solidlsp_settings),
"gradle-8.14.2",
)
)
if not os.path.exists(gradle_path):
FileUtils.download_and_extract_archive(
runtime_dependencies["gradle"]["platform-agnostic"]["url"],
str(PurePath(gradle_path).parent),
runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
gradle_path = str(
PurePath(
ls_resources_dir,
"gradle-8.14.2",
)
)
assert os.path.exists(gradle_path)
if not os.path.exists(gradle_path):
FileUtils.download_and_extract_archive(
runtime_dependencies["gradle"]["platform-agnostic"]["url"],
str(PurePath(gradle_path).parent),
runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
os.makedirs(vscode_java_path, exist_ok=True)
jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"]))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"]))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"]))
if not all(
[
os.path.exists(vscode_java_path),
os.path.exists(jre_home_path),
os.path.exists(jre_path),
os.path.exists(lombok_jar_path),
os.path.exists(jdtls_launcher_jar_path),
os.path.exists(jdtls_readonly_config_path),
assert os.path.exists(gradle_path)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"]))
os.makedirs(vscode_java_path, exist_ok=True)
jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"]))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"]))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"]))
if not all(
[
os.path.exists(vscode_java_path),
os.path.exists(jre_home_path),
os.path.exists(jre_path),
os.path.exists(lombok_jar_path),
os.path.exists(jdtls_launcher_jar_path),
os.path.exists(jdtls_readonly_config_path),
]
):
FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"])
os.chmod(jre_path, 0o755)
assert os.path.exists(vscode_java_path)
assert os.path.exists(jre_home_path)
assert os.path.exists(jre_path)
assert os.path.exists(lombok_jar_path)
assert os.path.exists(jdtls_launcher_jar_path)
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"]))
os.makedirs(intellicode_directory_path, exist_ok=True)
intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
if not all(
[
os.path.exists(intellicode_directory_path),
os.path.exists(intellicode_jar_path),
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"])
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
assert os.path.exists(intellisense_members_path)
return RuntimeDependencyPaths(
gradle_path=gradle_path,
lombok_jar_path=lombok_jar_path,
jre_path=jre_path,
jre_home_path=jre_home_path,
jdtls_launcher_jar_path=jdtls_launcher_jar_path,
jdtls_readonly_config_path=jdtls_readonly_config_path,
intellicode_jar_path=intellicode_jar_path,
intellisense_members_path=intellisense_members_path,
)
def create_launch_command(self) -> list[str] | str:
# ws_dir is the workspace directory for the EclipseJDTLS server
ws_dir = str(
PurePath(
self._solidlsp_settings.ls_resources_dir,
"EclipseJDTLS",
"workspaces",
uuid.uuid4().hex,
)
)
# shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
shared_cache_location = str(PurePath(self._solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex"))
os.makedirs(shared_cache_location, exist_ok=True)
os.makedirs(ws_dir, exist_ok=True)
jre_path = self.runtime_dependency_paths.jre_path
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
data_dir = str(PurePath(ws_dir, "data_dir"))
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path
if not os.path.exists(jdtls_config_path):
shutil.copytree(jdtls_readonly_config_path, jdtls_config_path)
for static_path in [
jre_path,
lombok_jar_path,
jdtls_launcher_jar,
jdtls_config_path,
jdtls_readonly_config_path,
]:
assert os.path.exists(static_path), static_path
cmd = [
jre_path,
"--add-modules=ALL-SYSTEM",
"--add-opens",
"java.base/java.util=ALL-UNNAMED",
"--add-opens",
"java.base/java.lang=ALL-UNNAMED",
"--add-opens",
"java.base/sun.nio.fs=ALL-UNNAMED",
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
"-Dosgi.bundles.defaultStartLevel=4",
"-Declipse.product=org.eclipse.jdt.ls.core.product",
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
"-Dfile.encoding=utf8",
"-noverify",
"-XX:+UseParallelGC",
"-XX:GCTimeRatio=4",
"-XX:AdaptiveSizePolicyWeight=90",
"-Dsun.zip.disableMemoryMapping=true",
"-Djava.lsp.joinOnCompletion=true",
"-Xmx3G",
"-Xms100m",
"-Xlog:disable",
"-Dlog.level=ALL",
f"-javaagent:{lombok_jar_path}",
f"-Djdt.core.sharedIndexLocation={shared_cache_location}",
"-jar",
f"{jdtls_launcher_jar}",
"-configuration",
f"{jdtls_config_path}",
"-data",
f"{data_dir}",
]
):
FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"])
os.chmod(jre_path, 0o755)
return cmd
assert os.path.exists(vscode_java_path)
assert os.path.exists(jre_home_path)
assert os.path.exists(jre_path)
assert os.path.exists(lombok_jar_path)
assert os.path.exists(jdtls_launcher_jar_path)
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
os.makedirs(intellicode_directory_path, exist_ok=True)
intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
if not all(
[
os.path.exists(intellicode_directory_path),
os.path.exists(intellicode_jar_path),
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"])
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
assert os.path.exists(intellisense_members_path)
return RuntimeDependencyPaths(
gradle_path=gradle_path,
lombok_jar_path=lombok_jar_path,
jre_path=jre_path,
jre_home_path=jre_home_path,
jdtls_launcher_jar_path=jdtls_launcher_jar_path,
jdtls_readonly_config_path=jdtls_readonly_config_path,
intellicode_jar_path=intellicode_jar_path,
intellisense_members_path=intellisense_members_path,
)
def create_launch_command_env(self) -> dict[str, str]:
return {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path}
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
+49 -49
View File
@@ -10,11 +10,10 @@ from time import sleep
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..lsp_protocol_handler import lsp_types
@@ -37,70 +36,71 @@ class Intelephense(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in self._ignored_dirnames
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Setup runtime dependencies for Intelephense and return the command to start the server.
"""
platform_id = PlatformUtils.get_platform_id()
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Setup runtime dependencies for Intelephense and return the path to the executable.
"""
platform_id = PlatformUtils.get_platform_id()
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by {cls.__name__} at the moment"
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by Intelephense at the moment"
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
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."
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
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."
# Install intelephense if not already installed
intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp")
os.makedirs(intelephense_ls_dir, exist_ok=True)
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
if not os.path.exists(intelephense_executable_path):
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="intelephense",
command="npm install --prefix ./ intelephense@1.14.4",
platform_id="any",
)
]
)
deps.install(intelephense_ls_dir)
# Install intelephense if not already installed
intelephense_ls_dir = os.path.join(self._ls_resources_dir, "php-lsp")
os.makedirs(intelephense_ls_dir, exist_ok=True)
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
if not os.path.exists(intelephense_executable_path):
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="intelephense",
command="npm install --prefix ./ intelephense@1.14.4",
platform_id="any",
)
]
)
deps.install(intelephense_ls_dir)
assert os.path.exists(
intelephense_executable_path
), f"intelephense executable not found at {intelephense_executable_path}, something went wrong."
assert os.path.exists(
intelephense_executable_path
), f"intelephense executable not found at {intelephense_executable_path}, something went wrong."
return [intelephense_executable_path, "--stdio"]
return intelephense_executable_path
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
intelephense_cmd = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php", solidlsp_settings
)
super().__init__(config, repository_root_path, None, "php", solidlsp_settings)
self.request_id = 0
# For PHP projects, we should ignore:
# - node_modules: if the project has JavaScript components
# - cache: commonly used for caching
# - (configurable) vendor: third-party dependencies <managed by Composer
# - (configurable) vendor: third-party dependencies managed by Composer
self._ignored_dirnames = {"node_modules", "cache"}
if self._custom_settings.get("ignore_vendor", True):
self._ignored_dirnames.add("vendor")
log.info(f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}")
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialization params for the Intelephense Language Server.
@@ -38,14 +38,13 @@ import platform
import shutil
import threading
import zipfile
from typing import Any, cast
from typing import Any, Union, cast
import requests
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -75,304 +74,306 @@ class MatlabLanguageServer(SolidLanguageServer):
- matlab_path: Path to MATLAB installation (overrides MATLAB_PATH env var)
"""
@staticmethod
def _find_matlab_installation() -> str:
"""
Find MATLAB installation path.
Search order:
1. MATLAB_PATH environment variable
2. Common installation locations based on platform
Returns:
Path to MATLAB installation directory.
Raises:
RuntimeError: If MATLAB installation is not found.
"""
# Check environment variable first
matlab_path = os.environ.get(MATLAB_PATH_ENV_VAR)
if matlab_path and os.path.isdir(matlab_path):
log.info(f"Using MATLAB from environment variable {MATLAB_PATH_ENV_VAR}: {matlab_path}")
return matlab_path
system = platform.system()
if system == "Darwin": # macOS
# Check common macOS locations
search_patterns = [
"/Applications/MATLAB_*.app",
"/Volumes/*/Applications/MATLAB_*.app",
os.path.expanduser("~/Applications/MATLAB_*.app"),
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True) # Newest version first
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
elif system == "Windows":
# Check common Windows locations
search_patterns = [
"C:\\Program Files\\MATLAB\\R*",
"C:\\Program Files (x86)\\MATLAB\\R*",
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True)
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
elif system == "Linux":
# Check common Linux locations
search_patterns = [
"/usr/local/MATLAB/R*",
"/opt/MATLAB/R*",
os.path.expanduser("~/MATLAB/R*"),
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True)
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
raise RuntimeError(
f"MATLAB installation not found. Set the {MATLAB_PATH_ENV_VAR} environment variable "
"to your MATLAB installation directory (e.g., /Applications/MATLAB_R2024b.app on macOS, "
"C:\\Program Files\\MATLAB\\R2024b on Windows, or /usr/local/MATLAB/R2024b on Linux)."
)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a MatlabLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
matlab_lsp_command, matlab_path = self._setup_runtime_dependencies(config, solidlsp_settings)
self._matlab_path = matlab_path
# Set environment for MATLAB
proc_env = {
"MATLAB_INSTALL_PATH": matlab_path,
}
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=matlab_lsp_command, cwd=repository_root_path, env=proc_env),
None,
"matlab",
solidlsp_settings,
)
assert isinstance(self._dependency_provider, self.DependencyProvider)
self._matlab_path = self._dependency_provider.get_matlab_path()
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
@classmethod
def _download_matlab_extension(cls, url: str, target_dir: str) -> bool:
"""
Download and extract the MATLAB extension from VS Code marketplace.
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
This method downloads the VSIX file and extracts it to get the language server.
class DependencyProvider(LanguageServerDependencyProvider):
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str):
super().__init__(custom_settings, ls_resources_dir)
self._matlab_path: str | None = None
Args:
url: VS Code marketplace URL for the MATLAB extension
target_dir: Directory where the extension will be extracted
@classmethod
def _download_matlab_extension(cls, url: str, target_dir: str) -> bool:
"""
Download and extract the MATLAB extension from VS Code marketplace.
Returns:
True if successful, False otherwise
The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
This method downloads the VSIX file and extracts it to get the language server.
"""
try:
log.info(f"Downloading MATLAB extension from {url}")
Args:
url: VS Code marketplace URL for the MATLAB extension
target_dir: Directory where the extension will be extracted
# Create target directory for the extension
os.makedirs(target_dir, exist_ok=True)
Returns:
True if successful, False otherwise
# Download with proper headers to mimic VS Code marketplace client
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/octet-stream, application/vsix, */*",
}
"""
try:
log.info(f"Downloading MATLAB extension from {url}")
response = requests.get(url, headers=headers, stream=True, timeout=300)
response.raise_for_status()
# Create target directory for the extension
os.makedirs(target_dir, exist_ok=True)
# Save to temporary VSIX file
temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
# Download with proper headers to mimic VS Code marketplace client
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/octet-stream, application/vsix, */*",
}
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
response = requests.get(url, headers=headers, stream=True, timeout=300)
response.raise_for_status()
with open(temp_file, "wb") as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0:
progress = (downloaded / total_size) * 100
log.info(f"Download progress: {progress:.1f}%")
# Save to temporary VSIX file
temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
log.info("Download complete, extracting...")
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
# Extract VSIX file (VSIX files are ZIP archives)
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(target_dir)
with open(temp_file, "wb") as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0:
progress = (downloaded / total_size) * 100
log.info(f"Download progress: {progress:.1f}%")
# Clean up temp file
os.remove(temp_file)
log.info("Download complete, extracting...")
log.info("MATLAB extension extracted successfully")
return True
# Extract VSIX file (VSIX files are ZIP archives)
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(target_dir)
except Exception as e:
log.error(f"Error downloading/extracting MATLAB extension: {e}")
return False
# Clean up temp file
os.remove(temp_file)
@classmethod
def _find_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""
Find MATLAB extension in various locations.
log.info("MATLAB extension extracted successfully")
return True
Search order:
1. Environment variable (MATLAB_EXTENSION_PATH)
2. Default download location (~/.serena/ls_resources/matlab-extension)
3. VS Code installed extensions
except Exception as e:
log.error(f"Error downloading/extracting MATLAB extension: {e}")
return False
Returns:
Path to MATLAB extension directory or None if not found
def _find_matlab_extension(self) -> str | None:
"""
Find MATLAB extension in various locations.
"""
# Check environment variable
env_path = os.environ.get("MATLAB_EXTENSION_PATH")
if env_path and os.path.exists(env_path):
log.debug(f"Found MATLAB extension via MATLAB_EXTENSION_PATH: {env_path}")
return env_path
elif env_path:
log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}")
Search order:
1. Environment variable (MATLAB_EXTENSION_PATH)
2. Default download location (~/.serena/ls_resources/matlab-extension)
3. VS Code installed extensions
# Check default download location
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension", "extension")
if os.path.exists(default_path):
log.debug(f"Found MATLAB extension in default location: {default_path}")
return default_path
Returns:
Path to MATLAB extension directory or None if not found
# Search VS Code extensions
vscode_extensions_dir = os.path.expanduser("~/.vscode/extensions")
if os.path.exists(vscode_extensions_dir):
for entry in os.listdir(vscode_extensions_dir):
if entry.startswith("mathworks.language-matlab"):
ext_path = os.path.join(vscode_extensions_dir, entry)
if os.path.isdir(ext_path):
log.debug(f"Found MATLAB extension in VS Code: {ext_path}")
return ext_path
"""
# Check environment variable
env_path = os.environ.get("MATLAB_EXTENSION_PATH")
if env_path and os.path.exists(env_path):
log.debug(f"Found MATLAB extension via MATLAB_EXTENSION_PATH: {env_path}")
return env_path
elif env_path:
log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}")
log.debug("MATLAB extension not found in any known location")
return None
# Check default download location
default_path = os.path.join(self._ls_resources_dir, "matlab-extension", "extension")
if os.path.exists(default_path):
log.debug(f"Found MATLAB extension in default location: {default_path}")
return default_path
@classmethod
def _download_and_install_matlab_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""
Download and install MATLAB extension from VS Code marketplace.
# Search VS Code extensions
vscode_extensions_dir = os.path.expanduser("~/.vscode/extensions")
if os.path.exists(vscode_extensions_dir):
for entry in os.listdir(vscode_extensions_dir):
if entry.startswith("mathworks.language-matlab"):
ext_path = os.path.join(vscode_extensions_dir, entry)
if os.path.isdir(ext_path):
log.debug(f"Found MATLAB extension in VS Code: {ext_path}")
return ext_path
Returns:
Path to installed extension or None if download failed
log.debug("MATLAB extension not found in any known location")
return None
"""
matlab_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "matlab-extension")
def _download_and_install_matlab_extension(self) -> str | None:
"""
Download and install MATLAB extension from VS Code marketplace.
log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}")
Returns:
Path to installed extension or None if download failed
if cls._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir):
extension_path = os.path.join(matlab_extension_dir, "extension")
if os.path.exists(extension_path):
log.info("MATLAB extension downloaded and installed successfully")
return extension_path
"""
matlab_extension_dir = os.path.join(self._ls_resources_dir, "matlab-extension")
log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}")
if self._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir):
extension_path = os.path.join(matlab_extension_dir, "extension")
if os.path.exists(extension_path):
log.info("MATLAB extension downloaded and installed successfully")
return extension_path
else:
log.error(f"Download completed but extension not found at: {extension_path}")
else:
log.error(f"Download completed but extension not found at: {extension_path}")
else:
log.error("Failed to download MATLAB extension from marketplace")
log.error("Failed to download MATLAB extension from marketplace")
return None
return None
@classmethod
def _get_executable_path(cls, extension_path: str, system: str) -> str:
"""
Get the path to the MATLAB language server executable based on platform.
@classmethod
def _get_executable_path(cls, extension_path: str) -> str:
"""
Get the path to the MATLAB language server executable based on platform.
The language server is a Node.js script located in the extension's server directory.
"""
# The MATLAB extension bundles the language server in the 'server' directory
server_dir = os.path.join(extension_path, "server", "out")
main_script = os.path.join(server_dir, "index.js")
The language server is a Node.js script located in the extension's server directory.
"""
# The MATLAB extension bundles the language server in the 'server' directory
server_dir = os.path.join(extension_path, "server", "out")
main_script = os.path.join(server_dir, "index.js")
if os.path.exists(main_script):
return main_script
if os.path.exists(main_script):
return main_script
# Alternative location
alt_script = os.path.join(extension_path, "out", "index.js")
if os.path.exists(alt_script):
return alt_script
# Alternative location
alt_script = os.path.join(extension_path, "out", "index.js")
if os.path.exists(alt_script):
return alt_script
raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}")
raise RuntimeError(f"MATLAB language server script not found in extension at {extension_path}")
@classmethod
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str]:
"""
Setup runtime dependencies for MATLAB Language Server and return the command to start the server.
@staticmethod
def _find_matlab_installation() -> str:
"""
Find MATLAB installation path.
Returns:
Tuple of (command to start the server, MATLAB installation path)
Search order:
1. MATLAB_PATH environment variable
2. Common installation locations based on platform
"""
system = platform.system()
Returns:
Path to MATLAB installation directory.
# Verify node is installed
node_path = shutil.which("node")
if node_path is None:
raise RuntimeError("Node.js is not installed or isn't in PATH. Please install Node.js and try again.")
Raises:
RuntimeError: If MATLAB installation is not found.
# Get MATLAB path from settings or auto-detect
language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
matlab_path = language_specific_config.get("matlab_path")
"""
# Check environment variable first
matlab_path = os.environ.get(MATLAB_PATH_ENV_VAR)
if matlab_path and os.path.isdir(matlab_path):
log.info(f"Using MATLAB from environment variable {MATLAB_PATH_ENV_VAR}: {matlab_path}")
return matlab_path
if not matlab_path:
matlab_path = cls._find_matlab_installation() # Raises RuntimeError if not found
system = platform.system()
# Verify MATLAB path exists
if not os.path.isdir(matlab_path):
raise RuntimeError(f"MATLAB installation directory does not exist: {matlab_path}")
if system == "Darwin": # macOS
# Check common macOS locations
search_patterns = [
"/Applications/MATLAB_*.app",
"/Volumes/*/Applications/MATLAB_*.app",
os.path.expanduser("~/Applications/MATLAB_*.app"),
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True) # Newest version first
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
log.info(f"Using MATLAB installation: {matlab_path}")
elif system == "Windows":
# Check common Windows locations
search_patterns = [
"C:\\Program Files\\MATLAB\\R*",
"C:\\Program Files (x86)\\MATLAB\\R*",
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True)
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
# Find existing extension or download if needed
extension_path = cls._find_matlab_extension(solidlsp_settings)
if extension_path is None:
log.info("MATLAB extension not found on disk, attempting to download...")
extension_path = cls._download_and_install_matlab_extension(solidlsp_settings)
elif system == "Linux":
# Check common Linux locations
search_patterns = [
"/usr/local/MATLAB/R*",
"/opt/MATLAB/R*",
os.path.expanduser("~/MATLAB/R*"),
]
for pattern in search_patterns:
matches = sorted(glob.glob(pattern), reverse=True)
for match in matches:
if os.path.isdir(match):
log.info(f"Found MATLAB installation: {match}")
return match
if extension_path is None:
raise RuntimeError(
"Failed to locate or download MATLAB Language Server. Please either:\n"
"1. Set MATLAB_EXTENSION_PATH environment variable to the MATLAB extension directory\n"
"2. Install the MATLAB extension in VS Code (MathWorks.language-matlab)\n"
"3. Ensure internet connection for automatic download"
f"MATLAB installation not found. Set the {MATLAB_PATH_ENV_VAR} environment variable "
"to your MATLAB installation directory (e.g., /Applications/MATLAB_R2024b.app on macOS, "
"C:\\Program Files\\MATLAB\\R2024b on Windows, or /usr/local/MATLAB/R2024b on Linux)."
)
# Get the language server script path
server_script = cls._get_executable_path(extension_path, system)
def get_matlab_path(self) -> str:
"""Get MATLAB path from settings or auto-detect."""
if self._matlab_path is not None:
return self._matlab_path
if not os.path.exists(server_script):
raise RuntimeError(f"MATLAB Language Server script not found at: {server_script}")
matlab_path = self._custom_settings.get("matlab_path")
# Build the command to run the language server
# The MATLAB language server is run via Node.js with the --stdio flag
cmd = [node_path, server_script, "--stdio"]
if not matlab_path:
matlab_path = self._find_matlab_installation() # Raises RuntimeError if not found
return cmd, matlab_path
# Verify MATLAB path exists
if not os.path.isdir(matlab_path):
raise RuntimeError(f"MATLAB installation directory does not exist: {matlab_path}")
log.info(f"Using MATLAB installation: {matlab_path}")
self._matlab_path = matlab_path
return matlab_path
def create_launch_command(self) -> Union[str, list[str]]:
# Verify node is installed
node_path = shutil.which("node")
if node_path is None:
raise RuntimeError("Node.js is not installed or isn't in PATH. Please install Node.js and try again.")
# Find existing extension or download if needed
extension_path = self._find_matlab_extension()
if extension_path is None:
log.info("MATLAB extension not found on disk, attempting to download...")
extension_path = self._download_and_install_matlab_extension()
if extension_path is None:
raise RuntimeError(
"Failed to locate or download MATLAB Language Server. Please either:\n"
"1. Set MATLAB_EXTENSION_PATH environment variable to the MATLAB extension directory\n"
"2. Install the MATLAB extension in VS Code (MathWorks.language-matlab)\n"
"3. Ensure internet connection for automatic download"
)
# Get the language server script path
server_script = self._get_executable_path(extension_path)
if not os.path.exists(server_script):
raise RuntimeError(f"MATLAB Language Server script not found at: {server_script}")
# Build the command to run the language server
# The MATLAB language server is run via Node.js with the --stdio flag
cmd = [node_path, server_script, "--stdio"]
return cmd
def create_launch_command_env(self) -> dict[str, str]:
return {
"MATLAB_INSTALL_PATH": self.get_matlab_path(),
}
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
@@ -6,15 +6,15 @@ import logging
import os
import pathlib
import re
import sys
import threading
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -34,7 +34,7 @@ class PyrightServer(SolidLanguageServer):
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
None,
"python",
solidlsp_settings,
)
@@ -43,6 +43,16 @@ class PyrightServer(SolidLanguageServer):
self.analysis_complete = threading.Event()
self.found_source_files = False
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
return sys.executable
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "-m", "pyright.langserver", "--stdio"]
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
@@ -13,11 +13,10 @@ from overrides import override
from sensai.util.logging import LogTime
from solidlsp import ls_types
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -72,17 +71,19 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"""
Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
ts_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path),
None,
"typescript",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [
@@ -97,91 +98,96 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"""Classify typescript-language-server stderr output to avoid false-positive errors."""
return SolidLanguageServer._determine_log_level(line)
@classmethod
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Setup runtime dependencies for TypeScript Language Server and return the command to start the server.
"""
platform_id = PlatformUtils.get_platform_id()
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Setup runtime dependencies for TypeScript Language Server and return the path to the executable.
"""
platform_id = PlatformUtils.get_platform_id()
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment"
# Get version settings from ls_specific_settings or use defaults
language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
typescript_version = language_specific_config.get("typescript_version", "5.9.3")
typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="typescript",
description="typescript package",
command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="typescript-language-server package",
command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"],
platform_id="any",
),
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
)
assert (
platform_id in valid_platforms
), f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment"
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
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."
# Get version settings from ls_specific_settings or use defaults
language_specific_config = self._custom_settings
typescript_version = language_specific_config.get("typescript_version", "5.9.3")
typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3")
# Install typescript and typescript-language-server if not already installed or version mismatch
tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp")
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
# Check if installation is needed based on executable AND version
version_file = os.path.join(tsserver_ls_dir, ".installed_version")
expected_version = f"{typescript_version}_{typescript_language_server_version}"
needs_install = False
if not os.path.exists(tsserver_executable_path):
log.info(f"Typescript Language Server executable not found at {tsserver_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"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
)
needs_install = True
else:
# No version file exists, assume old installation needs refresh
log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...")
needs_install = True
if needs_install:
log.info("Installing TypeScript Language Server dependencies...")
with LogTime("Installation of TypeScript language server dependencies", logger=log):
deps.install(tsserver_ls_dir)
# Write version marker file
with open(version_file, "w") as f:
f.write(expected_version)
log.info("TypeScript language server dependencies installed successfully")
if not os.path.exists(tsserver_executable_path):
raise FileNotFoundError(
f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation."
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="typescript",
description="typescript package",
command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="typescript-language-server package",
command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"],
platform_id="any",
),
]
)
return [tsserver_executable_path, "--stdio"]
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
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."
# Install typescript and typescript-language-server if not already installed or version mismatch
tsserver_ls_dir = os.path.join(self._ls_resources_dir, "ts-lsp")
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
# Check if installation is needed based on executable AND version
version_file = os.path.join(tsserver_ls_dir, ".installed_version")
expected_version = f"{typescript_version}_{typescript_language_server_version}"
needs_install = False
if not os.path.exists(tsserver_executable_path):
log.info(f"Typescript Language Server executable not found at {tsserver_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"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
)
needs_install = True
else:
# No version file exists, assume old installation needs refresh
log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...")
needs_install = True
if needs_install:
log.info("Installing TypeScript Language Server dependencies...")
with LogTime("Installation of TypeScript language server dependencies", logger=log):
deps.install(tsserver_ls_dir)
# Write version marker file
with open(version_file, "w") as f:
f.write(expected_version)
log.info("TypeScript language server dependencies installed successfully")
if not os.path.exists(tsserver_executable_path):
raise FileNotFoundError(
f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation."
)
return tsserver_executable_path
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
@@ -36,8 +36,6 @@ log = logging.getLogger(__name__)
class VueTypeScriptServer(TypeScriptLanguageServer):
"""TypeScript LS configured with @vue/typescript-plugin for Vue file support."""
_pending_ts_ls_executable: list[str] | None = None
@classmethod
@override
def get_language_enum_instance(cls) -> Language:
@@ -49,12 +47,13 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
"""
return Language.TYPESCRIPT
@classmethod
@override
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
if cls._pending_ts_ls_executable is not None:
return cls._pending_ts_ls_executable
return ["typescript-language-server", "--stdio"]
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
override_ts_ls_executable: str | None = None
def _get_or_install_core_dependency(self) -> str:
if self.override_ts_ls_executable is not None:
return self.override_ts_ls_executable
return super()._get_or_install_core_dependency()
@override
def _get_language_id_for_file(self, relative_file_path: str) -> str:
@@ -81,13 +80,13 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
solidlsp_settings: SolidLSPSettings,
vue_plugin_path: str,
tsdk_path: str,
ts_ls_executable_path: list[str],
ts_ls_executable_path: str,
):
self._vue_plugin_path = vue_plugin_path
self._custom_tsdk_path = tsdk_path
VueTypeScriptServer._pending_ts_ls_executable = ts_ls_executable_path
VueTypeScriptServer.DependencyProvider.override_ts_ls_executable = ts_ls_executable_path
super().__init__(config, repository_root_path, solidlsp_settings)
VueTypeScriptServer._pending_ts_ls_executable = None
VueTypeScriptServer.DependencyProvider.override_ts_ls_executable = None
@override
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
@@ -383,9 +382,7 @@ class VueLanguageServer(SolidLanguageServer):
return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name)
@classmethod
def _setup_runtime_dependencies(
cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> tuple[list[str], str, list[str]]:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str, str]:
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
is_npm_installed = shutil.which("npm") is not None
@@ -476,7 +473,7 @@ class VueLanguageServer(SolidLanguageServer):
f"typescript-language-server executable not found at {ts_ls_executable_path}, something went wrong with the installation."
)
return [vue_executable_path, "--stdio"], tsdk_path, [ts_ls_executable_path, "--stdio"]
return [vue_executable_path, "--stdio"], tsdk_path, ts_ls_executable_path
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
+94 -4
View File
@@ -130,6 +130,72 @@ class DocumentSymbols:
return self._all_symbols, self.root_symbols
class LanguageServerDependencyProvider(ABC):
"""
Prepares dependencies for a language server (if any), ultimately enabling the launch command to be constructed
and optionally providing environment variables that are necessary for the execution.
"""
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str):
self._custom_settings = custom_settings
self._ls_resources_dir = ls_resources_dir
@abstractmethod
def create_launch_command(self) -> list[str] | str:
"""
Creates the launch command for this language server, potentially downloading and installing dependencies
beforehand.
:return: the launch command as a list containing the executable and its arguments (preferred for robustness)
or the entire command in a single string.
"""
def create_launch_command_env(self) -> dict[str, str]:
"""
Provides environment variables to be set when executing the launch command.
This method is intended to be overridden by subclasses that need to set variables.
:return: a mapping for variable names to values
"""
return {}
class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvider, ABC):
"""
Special case of a dependency provider, where there is a single core dependency which provides
the basis for the launch command.
The core dependency's path can be overridden by the user in LS-specific settings (SerenaConfig)
via the key "ls_path". If the user provides the key, the specified path is used directly.
Otherwise, the provider implementation is called to get or install the core dependency.
"""
@abstractmethod
def _get_or_install_core_dependency(self) -> str:
"""
Gets the language server's core path, potentially installing dependencies beforehand.
:return: the core dependency's path (e.g. executable, jar, etc.)
"""
def create_launch_command(self) -> Union[str, list[str]]:
path = self._custom_settings.get("ls_path", None)
if path is not None:
core_path = path
else:
core_path = self._get_or_install_core_dependency()
return self._create_launch_command(core_path)
@abstractmethod
def _create_launch_command(self, core_path: str) -> list[str] | str:
"""
:param core_path: path to the core dependency
:return: the launch command as a list containing the executable and its arguments (preferred for robustness)
or the entire command in a single string.
"""
class SolidLanguageServer(ABC):
"""
The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
@@ -242,7 +308,7 @@ class SolidLanguageServer(ABC):
self,
config: LanguageServerConfig,
repository_root_path: str,
process_launch_info: ProcessLaunchInfo,
process_launch_info: ProcessLaunchInfo | None,
language_id: str,
solidlsp_settings: SolidLSPSettings,
cache_version_raw_document_symbols: Hashable = 1,
@@ -254,7 +320,8 @@ class SolidLanguageServer(ABC):
:param config: the global SolidLSP configuration.
:param repository_root_path: the root path of the repository.
:param process_launch_info: the command used to start the actual language server.
:param process_launch_info: (DEPRECATED - implement _create_dependency_provider instead)
the command used to start the actual language server.
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
as opposed to HTTP, TCP modes supported by some language servers.
:param cache_version_raw_document_symbols: the version, for caching, of the raw document symbols coming
@@ -265,9 +332,11 @@ class SolidLanguageServer(ABC):
self._solidlsp_settings = solidlsp_settings
lang = self.get_language_enum_instance()
self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang)
self._ls_resources_dir = self.ls_resources_dir(solidlsp_settings)
log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}")
self._encoding = config.encoding
self.repository_root_path: str = repository_root_path
log.debug(
f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}"
)
@@ -303,8 +372,12 @@ class SolidLanguageServer(ABC):
else:
logging_fn = None # type: ignore
# cmd is obtained from the child classes, which provide the language specific command to start the language server
# LanguageServerHandler provides the functionality to start the language server and communicate with it
# create the LanguageServerHandler, which provides the functionality to start the language server and communicate with it,
# preparing the launch command beforehand
self._dependency_provider: LanguageServerDependencyProvider | None = None
if process_launch_info is None:
self._dependency_provider = self._create_dependency_provider()
process_launch_info = self._create_process_launch_info()
log.debug(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}")
self.server = SolidLanguageServerHandler(
process_launch_info,
@@ -330,6 +403,23 @@ class SolidLanguageServer(ABC):
self._has_waited_for_cross_file_references = False
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
"""
Creates the dependency provider for this language server.
Subclasses should override this method to provide their specific dependency provider.
This method is only called if process_launch_info is not passed to __init__.
"""
raise NotImplementedError(
f"{self.__class__.__name__} must implement _create_dependency_provider() or pass process_launch_info to __init__()"
)
def _create_process_launch_info(self) -> ProcessLaunchInfo:
assert self._dependency_provider is not None
cmd = self._dependency_provider.create_launch_command()
env = self._dependency_provider.create_launch_command_env()
return ProcessLaunchInfo(cmd=cmd, cwd=self.repository_root_path, env=env)
def _get_wait_time_for_cross_file_referencing(self) -> float:
"""Meant to be overridden by subclasses for LS that don't have a reliable "finished initializing" signal.
+2 -2
View File
@@ -237,7 +237,7 @@ class TestCSharpSolutionProjectOpening:
# Should still prefer .sln file even though it's deeper
assert result == str(solution_file)
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_solution_discovery(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs solution/project discovery during initialization."""
@@ -266,7 +266,7 @@ class TestCSharpSolutionProjectOpening:
expected_log_msg = f"Found solution/project file: {solution_file}"
assert expected_log_msg in mem_log.get_log()
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_no_solution_warning(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs warning when no solution/project files are found."""