mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 04:15:11 +00:00
Merge pull request #933 from oraios/ls-dependency-provider
Introduce LanguageServerDependencyProvider abstraction
This commit is contained in:
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
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user