mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 00:15:05 +00:00
Merge pull request #1636 from oraios/refactor-ls-dependency-providers
This commit is contained in:
5 files changed
+184
-73
No files matched your search
@@ -33,16 +33,30 @@ To implement a new language server using the DependencyProvider pattern:
|
||||
```
|
||||
The resource dir that is passed is the directory in which installed dependencies should be stored!
|
||||
|
||||
**Base Classes:**
|
||||
**Base Classes** (choose the most specific one that fits):
|
||||
|
||||
- **`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)
|
||||
- **`LanguageServerDependencyProviderUvx`** - For language servers distributed as a PyPI package, run on demand
|
||||
via `uvx` / `uv x` (no installation step to implement)
|
||||
- Simply instantiate it with the package name, pinned default version, entrypoint (console script) and optional `extra_args`;
|
||||
the version can be overridden by the user via the configured `version_setting_key` custom setting
|
||||
- Reference implementation: `PyrightServer`
|
||||
|
||||
- **`LanguageServerDependencyProviderBaseCommand`** - For the common case where the
|
||||
launch command is constructed from a *base command* (which the user can override via custom settings; handled generically)
|
||||
- Implement `_create_default_base_command()` to return the default base command (executable + args), downloading/installing
|
||||
dependencies beforehand if necessary
|
||||
- Implement `_create_launch_command_from_base_command(base_command)` to add any further arguments, producing the
|
||||
final launch command
|
||||
|
||||
- **`LanguageServerDependencyProviderSinglePath`** - Alternative to inheriting from `...BaseCommand` directly for the case
|
||||
of a single core dependency (e.g., an executable or JAR file); mostly present in existing implementations - for new
|
||||
implementations, prefer `...BaseCommand`
|
||||
- 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`
|
||||
- Reference implementations: `TypeScriptLanguageServer`, `Intelephense`, `ClojureLSP`, `ClangdLanguageServer`
|
||||
|
||||
- **`LanguageServerDependencyProvider`** - The base class, which can be directly inherited from for complex cases with multiple dependencies or custom setup
|
||||
- Implement `create_launch_command()` directly
|
||||
- **`LanguageServerDependencyProvider`** - The root base class, for complex cases with multiple dependencies or custom setup
|
||||
- Implement `create_launch_command()` directly (note: no automatic support for user-level launch command overrides in this case)
|
||||
- Reference implementations: `EclipseJDTLS`, `CSharpLanguageServer`, `MatlabLanguageServer`
|
||||
|
||||
**Implementation Pointers::**
|
||||
|
||||
@@ -62,6 +62,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Improve quoting of arguments in shell executions
|
||||
- Add **LaTeX** support (experimental) via [texlab](https://github.com/latex-lsp/texlab).
|
||||
- PHP: add support for PHPantom as alternative to the already supported PHP LS #1554.
|
||||
- Add new launch command customization options: `ls_args`, `ls_extra_args` and `ls_base_cmd`
|
||||
|
||||
* JetBrains:
|
||||
- Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically
|
||||
|
||||
@@ -216,31 +216,39 @@ ls_specific_settings:
|
||||
```
|
||||
|
||||
(override-ls-path)=
|
||||
#### Overriding the Language Server Path
|
||||
#### Customizing the Language Server Launch Command
|
||||
|
||||
Most of Serena's 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:
|
||||
Most of Serena's language servers construct the command that launches the language server process from
|
||||
a *base command* or a *core dependency*.
|
||||
For these language servers, the following settings can be used to customize the launch command:
|
||||
|
||||
| Setting | Description |
|
||||
|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `ls_path` (string) or `ls_base_cmd` (list) | overrides the path of the language server's core dependency (`ls_path`), e.g. its executable or a JAR file, or a base command for its execution (`ls_base_cmd`), e.g. `["npx", "-y", "/my/local/package"]`. Use this if you have installed the language server yourself and want Serena to use your installation instead of its managed installation. |
|
||||
| `ls_args` (list) | overrides the internal command construction completely and simply adds `ls_args` to the base command |
|
||||
| `ls_extra_args` (list) | a list of additional arguments to append to the launch command |
|
||||
|
||||
* If you set `ls_args`, the internal command construction (which may do more than to append arguments to a base command) is bypassed.
|
||||
You can define the full launch command by providing both `ls_path`/`ls_base_cmd` and `ls_args`.
|
||||
* If `ls_args` is not set, the internal command construction (which sets default arguments) is applied, and you can use `ls_path` or `ls_base_cmd` to override the path of the core dependency/the base command.
|
||||
* `ls_extra_args` is always appended to the end of the launch command.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
<language>:
|
||||
ls_path: "/path/to/language-server"
|
||||
ls_extra_args: ["--log-level=debug"]
|
||||
```
|
||||
|
||||
This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`,
|
||||
and by some additional wrappers that explicitly expose `ls_path`.
|
||||
These settings are supported by all language servers whose dependency provider derives from
|
||||
`LanguageServerDependencyProviderBaseCommand`, and `ls_path` is additionally exposed by some implementations explicitly.
|
||||
Common examples include: `ansible`, `bash`, `bsl`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `html`, `kotlin`, `lean4`, `luau`, `markdown`, `php`,
|
||||
`php_phpactor`, `python`, `rust`, `scss`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`.
|
||||
|
||||
Note: `angular` does **not** support `ls_path` — the Angular language server is part of a multi-process orchestration
|
||||
(`ngserver` plus a companion TypeScript language server with the `@angular/language-service` plugin and an HTML
|
||||
companion) where the dependency layout matters; use the version overrides documented in the Angular section below
|
||||
to pin specific releases of the bundled stack.
|
||||
|
||||
If a language server supports `ls_path`, setting it bypasses Serena's managed download or install for that server.
|
||||
In that case, any server-specific version or registry settings only apply when `ls_path` is not set.
|
||||
If `ls_path` is set, Serena's managed download or install is bypassed for that language server.
|
||||
In that case, any server-specific version or registry settings do not apply.
|
||||
|
||||
#### AL
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
@@ -151,42 +150,6 @@ class RuntimeDependencyCollection:
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_UVX_PYTHON_VERSION = "3.13"
|
||||
|
||||
|
||||
def build_uvx_launch_command(
|
||||
package: str,
|
||||
version: str,
|
||||
entrypoint: str,
|
||||
extra_args: Sequence[str] = (),
|
||||
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
|
||||
) -> list[str]:
|
||||
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
|
||||
|
||||
Resolution order:
|
||||
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
|
||||
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
|
||||
3. Raise ``RuntimeError`` if neither is available.
|
||||
|
||||
:param package: PyPI package name (e.g. ``"pyright"``).
|
||||
:param version: Pinned package version.
|
||||
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
|
||||
:param extra_args: Arguments appended after the entrypoint (e.g. ``("--stdio",)``).
|
||||
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
|
||||
"""
|
||||
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint, *extra_args]
|
||||
|
||||
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
|
||||
if uvx_path is not None:
|
||||
return [uvx_path, *base_args]
|
||||
|
||||
uv_path = shutil.which("uv")
|
||||
if uv_path is not None:
|
||||
return [uv_path, "x", *base_args]
|
||||
|
||||
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
|
||||
|
||||
|
||||
def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]:
|
||||
"""Build a pinned npm install command for a package in a Serena-managed install directory."""
|
||||
command = ["npm", "install", "--prefix", "./"]
|
||||
|
||||
+141
-16
@@ -22,7 +22,6 @@ from sensai.util.string import ToStringMixin
|
||||
from serena.util.file_system import match_path
|
||||
from serena.util.text_utils import MatchedConsecutiveLines
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import build_uvx_launch_command
|
||||
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer
|
||||
@@ -300,7 +299,76 @@ class LanguageServerDependencyProvider(ABC):
|
||||
return {}
|
||||
|
||||
|
||||
class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvider, ABC):
|
||||
class LanguageServerDependencyProviderBaseCommand(LanguageServerDependencyProvider, ABC):
|
||||
"""
|
||||
Special case of a dependency provider, where the launch command is constructed from a base command.
|
||||
|
||||
The user can configure aspects of the launch command in LS-specific settings:
|
||||
|
||||
* ``ls_base_cmd`: overrides the base command
|
||||
* ``ls_path``: overrides the path of the language server's core dependency (e.g. its executable or a JAR file),
|
||||
from which the base command is formed, bypassing Serena's managed installation
|
||||
* ``ls_args``: overrides the arguments that are added to the base command in order to form the launch command
|
||||
* ``ls_extra_args``: additional arguments to append to the launch command
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _create_default_base_command(self) -> list[str]:
|
||||
"""
|
||||
Obtains the default base command for this language server, potentially downloading and installing dependencies
|
||||
beforehand.
|
||||
|
||||
Note: The user can override the base command, so this will only be run if no custom base command is provided.
|
||||
|
||||
:return: the base command as a list containing the executable and its arguments
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
|
||||
"""
|
||||
Adds any additional arguments to the base command to create the final launch command.
|
||||
|
||||
:param base_command: the base command
|
||||
:return: the extended command
|
||||
"""
|
||||
|
||||
def create_launch_command(self) -> list[str]:
|
||||
# obtain base command
|
||||
base_command = self._custom_settings.get("ls_base_cmd", None)
|
||||
if base_command is not None and not isinstance(base_command, list):
|
||||
log.warning("The 'ls_base_cmd' setting should be a list of strings. Ignoring the provided value: %s", base_command)
|
||||
base_command = None
|
||||
if base_command is None:
|
||||
ls_path = self._custom_settings.get("ls_path", None)
|
||||
if ls_path is not None:
|
||||
base_command = [ls_path]
|
||||
else:
|
||||
# default case: base command is constructed by the provider implementation
|
||||
base_command = self._create_default_base_command()
|
||||
|
||||
# create launch command from base command
|
||||
ls_args = self._custom_settings.get("ls_args")
|
||||
if ls_args is not None:
|
||||
if not isinstance(ls_args, list):
|
||||
log.warning("The 'ls_args' setting should be a list of strings. Ignoring the provided value: %s", ls_args)
|
||||
ls_args = None
|
||||
if ls_args is not None:
|
||||
cmd = list(base_command) + ls_args
|
||||
else:
|
||||
cmd = self._create_launch_command_from_base_command(list(base_command))
|
||||
|
||||
# add user-provided extra arguments (if any)
|
||||
ls_extra_args = self._custom_settings.get("ls_extra_args", [])
|
||||
if ls_extra_args:
|
||||
if not isinstance(ls_extra_args, list):
|
||||
log.warning("The 'ls_extra_args' setting should be a list of strings. Ignoring the provided value: %s", ls_extra_args)
|
||||
else:
|
||||
cmd = cmd + ls_extra_args
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProviderBaseCommand, ABC):
|
||||
"""
|
||||
Special case of a dependency provider, where there is a single core dependency which provides
|
||||
the basis for the launch command.
|
||||
@@ -308,6 +376,13 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide
|
||||
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.
|
||||
|
||||
Note: The inheritance from the BaseCommand class serves to allow user overrides to be handled
|
||||
centrally, but implementations of this class do not necessarily follow the principle that
|
||||
the base command is strictly extended.
|
||||
Yet incompatibility arises only if (a) the user overrides the base command with a command that
|
||||
has arguments, and (b) the implementation of this class does not construct the launch command
|
||||
by appending arguments to the core dependency's path.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -318,14 +393,6 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide
|
||||
:return: the core dependency's path (e.g. executable, jar, etc.)
|
||||
"""
|
||||
|
||||
def create_launch_command(self) -> 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]:
|
||||
"""
|
||||
@@ -333,8 +400,33 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide
|
||||
:return: the launch command as a list containing the executable and its arguments
|
||||
"""
|
||||
|
||||
def _create_default_base_command(self) -> list[str]:
|
||||
# We treat the core path as the only element of the base command,
|
||||
# noting that the construction of the launch command from the base command
|
||||
# is not necessarily to append arguments only.
|
||||
core_path = self._get_or_install_core_dependency()
|
||||
return [core_path]
|
||||
|
||||
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
|
||||
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
|
||||
core_path = base_command[0]
|
||||
cmd = self._create_launch_command(core_path)
|
||||
if len(base_command) == 1:
|
||||
# This is the regular case, where the base command consists only of the core
|
||||
# dependency's path, and the launch command is constructed from it.
|
||||
return cmd
|
||||
else:
|
||||
# In this case, the user has overridden the base command via LS-specific settings
|
||||
# with a command that has arguments and therefore does not map cleanly to
|
||||
# the single path assumption. However, in special cases where the full command
|
||||
# was constructed by appending arguments to the core dependency's path,
|
||||
# we simply assume that the provided base command can be substituted.
|
||||
if cmd[0] == core_path:
|
||||
return base_command + cmd[1:]
|
||||
else:
|
||||
raise ValueError("Language server base launch command with arguments unsupported")
|
||||
|
||||
|
||||
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProviderBaseCommand):
|
||||
"""
|
||||
Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``.
|
||||
|
||||
@@ -343,6 +435,8 @@ class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
|
||||
already-installed language server executable, in which case it is launched directly, bypassing uv entirely.
|
||||
"""
|
||||
|
||||
DEFAULT_UVX_PYTHON_VERSION = "3.13"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
custom_settings: "SolidLSPSettings.CustomLSSettings",
|
||||
@@ -368,12 +462,43 @@ class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
|
||||
self._version_setting_key = version_setting_key
|
||||
self._extra_args = tuple(extra_args)
|
||||
|
||||
def create_launch_command(self) -> list[str]:
|
||||
ls_path = self._custom_settings.get("ls_path")
|
||||
if ls_path is not None:
|
||||
return [ls_path, *self._extra_args]
|
||||
@staticmethod
|
||||
def _build_uvx_base_command(
|
||||
package: str,
|
||||
version: str,
|
||||
entrypoint: str,
|
||||
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
|
||||
) -> list[str]:
|
||||
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
|
||||
|
||||
Resolution order:
|
||||
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
|
||||
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
|
||||
3. Raise ``RuntimeError`` if neither is available.
|
||||
|
||||
:param package: PyPI package name (e.g. ``"pyright"``).
|
||||
:param version: Pinned package version.
|
||||
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
|
||||
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
|
||||
"""
|
||||
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint]
|
||||
|
||||
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
|
||||
if uvx_path is not None:
|
||||
return [uvx_path, *base_args]
|
||||
|
||||
uv_path = shutil.which("uv")
|
||||
if uv_path is not None:
|
||||
return [uv_path, "x", *base_args]
|
||||
|
||||
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
|
||||
|
||||
def _create_default_base_command(self):
|
||||
version = self._custom_settings.get(self._version_setting_key, self._default_version)
|
||||
return build_uvx_launch_command(self._package, version, self._entrypoint, self._extra_args)
|
||||
return self._build_uvx_base_command(self._package, version, self._entrypoint)
|
||||
|
||||
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
|
||||
return base_command + list(self._extra_args)
|
||||
|
||||
|
||||
class SolidLanguageServer(ABC):
|
||||
|
||||
Reference in new issue
Block a user