mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 22:14:54 +00:00
Move dependency providers to separate module
This commit is contained in:
1 parent
cb1a690419
commit
58477ffd64
2 files changed
+17
-3360
No files matched your search
File diff suppressed because it is too large.
Load diff
+16
-238
@@ -8,7 +8,7 @@ import shutil
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Hashable, Iterator, Sequence
|
||||
from collections.abc import Callable, Hashable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
@@ -17,12 +17,19 @@ from time import monotonic, perf_counter, sleep
|
||||
from typing import Any, Self, Union, cast
|
||||
|
||||
import pathspec
|
||||
from sensai.util.helper import mark_used
|
||||
from sensai.util.pickle import getstate, load_pickle
|
||||
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.dependency_provider import (
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderBaseCommand,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
LanguageServerDependencyProviderUvx,
|
||||
)
|
||||
from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder
|
||||
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
@@ -59,6 +66,14 @@ The `DocumentSymbol` is the preferred type, but the legacy type `SymbolInformati
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# backward compatibility (support old imports of these classes from this module)
|
||||
mark_used(
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderBaseCommand,
|
||||
LanguageServerDependencyProviderUvx,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
)
|
||||
|
||||
_debug_enabled = log.isEnabledFor(logging.DEBUG)
|
||||
"""Serves as a flag that triggers additional computation when debug logging is enabled."""
|
||||
|
||||
@@ -272,243 +287,6 @@ 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):
|
||||
"""
|
||||
:param custom_settings: the (user-provided) language server-specific settings
|
||||
:param ls_resources_dir: the directory in which data for the language-server shall be stored
|
||||
(this is already specific to the concrete language server, i.e. no further subdirectory is needed)
|
||||
"""
|
||||
self._custom_settings = custom_settings
|
||||
self._ls_resources_dir = ls_resources_dir
|
||||
|
||||
@abstractmethod
|
||||
def create_launch_command(self) -> list[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
|
||||
"""
|
||||
|
||||
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 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.
|
||||
|
||||
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
|
||||
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.)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
"""
|
||||
:param core_path: path to the core dependency
|
||||
: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]
|
||||
|
||||
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``.
|
||||
|
||||
The pinned package version can be overridden by the user via the LS-specific setting given by
|
||||
``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an
|
||||
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",
|
||||
ls_resources_dir: str,
|
||||
*,
|
||||
package: str,
|
||||
entrypoint: str,
|
||||
default_version: str,
|
||||
version_setting_key: str,
|
||||
extra_args: Sequence[str] = (),
|
||||
):
|
||||
"""
|
||||
:param package: the PyPI package name (e.g. ``"pyright"``)
|
||||
:param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``)
|
||||
:param default_version: the package version to pin unless overridden
|
||||
:param version_setting_key: the LS-specific setting key through which the user can override the version
|
||||
:param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``)
|
||||
"""
|
||||
super().__init__(custom_settings, ls_resources_dir)
|
||||
self._package = package
|
||||
self._entrypoint = entrypoint
|
||||
self._default_version = default_version
|
||||
self._version_setting_key = version_setting_key
|
||||
self._extra_args = tuple(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 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):
|
||||
"""
|
||||
High-level abstraction for language server interaction, which wraps the underlying low-level LSP interface
|
||||
|
||||
Reference in new issue
Block a user