mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 06:15:43 +00:00
Merge pull request #1988 from oraios/ls-registry
SolidLSP: Add language server registry, supporting externally provided implementations
This commit is contained in:
24 files changed
+323
-162
No files matched your search
@@ -56,6 +56,8 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
rest of the session (#1871)
|
||||
- Fix: Exceptions raised during `LanguageServerManager.start` did not stop the language server subprocess if it was
|
||||
already started (#1949)
|
||||
- Add: Installed Python packages can provide generic external language-server adapters through the
|
||||
`serena.language_servers` entry-point group for explicit use in `project.yml`
|
||||
- Fix: Dart's `$/analyzerStatus` notifications were logged as unhandled-method warnings during analysis (#1855)
|
||||
- Fix: `DartLanguageServer._start_server` discarded both `$/analyzerStatus` and
|
||||
`experimental/serverStatus`, the two notifications the Dart analysis server sends to report
|
||||
|
||||
@@ -190,8 +190,9 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **Zig**
|
||||
(requires installation of ZLS - Zig Language Server)
|
||||
|
||||
Support for further languages can easily be added by providing a shallow adapter for a new language server implementation,
|
||||
see Serena's [memory on that](https://github.com/oraios/serena/blob/main/.serena/memories/adding_new_language_support_guide.md).
|
||||
Additional language servers can easily be supported by providing an adapter implementation;
|
||||
see our [contribution guide](https://github.com/oraios/serena/blob/main/CONTRIBUTING.md).
|
||||
If you need to support a custom language server which is not yet publicly available, you have the option to [register an external language server](external-ls-registration).
|
||||
|
||||
## The Serena JetBrains Plugin
|
||||
|
||||
|
||||
@@ -45,8 +45,11 @@ within the project directory.
|
||||
|
||||
The file allows you to configure ...
|
||||
* the name by which you want to refer to the project (relevant when telling the LLM to dynamically activate the project)
|
||||
* the set of programming languages for which language servers are spawned (not relevant when using the JetBrains plugin)
|
||||
Note that you can dynamically add/remove language servers while Serena is running via the [Dashboard](060_dashboard).
|
||||
* the set of built-in language servers and explicitly registered external adapters for which servers are spawned
|
||||
(not relevant when using the JetBrains plugin). External adapter IDs become available after their Python package is installed;
|
||||
see [External Language Server Adapters](../03-special-guides/external_language_server_registration.md).
|
||||
The [Dashboard](060_dashboard) can dynamically add or remove built-in language servers and installed, registered external
|
||||
adapters while Serena is running. It does not install adapter packages or offer unregistered adapter IDs.
|
||||
* the [language backend](per-project-language-backend) to use for this project (overriding the global setting)
|
||||
* the encoding used in source files
|
||||
* ignore rules
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Special Guides
|
||||
|
||||
This section contains special guides for certain topics that require more in-depth explanations.
|
||||
This section contains special guides for certain topics that require more in-depth explanations
|
||||
or are too specific to be included in the main documentation. These guides are meant to provide additional context and instructions for users who need them.
|
||||
@@ -0,0 +1,55 @@
|
||||
(external-ls-registration)=
|
||||
# External Language Server Registration
|
||||
|
||||
SolidLSP, Serena's language server framework, is designed to be extensible.
|
||||
Sometimes, you may need to use Serena with a language server for which there can be no built-in adapter,
|
||||
e.g. because the language server in question is a custom development which is not yet released.
|
||||
|
||||
The general mechanism to make SolidLSP aware of an external language server is to use the
|
||||
`LanguageServerRegistry` in order to register an `ExternalLanguageServerId`.
|
||||
|
||||
You can either do this
|
||||
* programmatically, in your own Python code, if you are launching Serena via a custom script
|
||||
where this code can be executed before Serena itself is started, or
|
||||
* via a Python package that exposes a `solidlsp.language_server_registration` entry point.
|
||||
If the Python package is installed in the same environment as Serena,
|
||||
the language server will be automatically registered when Serena starts.
|
||||
|
||||
## Registering an External Language Server via a Python Package
|
||||
|
||||
Expose a Python entry point in the `solidlsp.language_server_registration` group (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[project.entry-points."solidlsp.language_server_registration"]
|
||||
example = "myls:register_myls"
|
||||
```
|
||||
|
||||
Corresponding contents of `myls/__init__.py`:
|
||||
|
||||
```python
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import FilenameMatcher, LanguageServerRegistry, ExternalLanguageServerId
|
||||
|
||||
|
||||
class MyLanguageServerImpl(SolidLanguageServer):
|
||||
# implementation of the language server adapter
|
||||
pass
|
||||
|
||||
|
||||
def register_myls() -> None:
|
||||
LanguageServerRegistry.get_instance().register(
|
||||
ExternalLanguageServerId(
|
||||
key="myls",
|
||||
matcher=FilenameMatcher(".example"),
|
||||
implementation=MyLanguageServerImpl
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
The registered key `myls` can subsequently be used in a project's configuration file (`project.yml`)
|
||||
like any other language server:
|
||||
|
||||
```yaml
|
||||
language_servers:
|
||||
- myls
|
||||
```
|
||||
+7
-7
@@ -61,7 +61,7 @@ from serena.tools import (
|
||||
from serena.util.gui import system_has_usable_display
|
||||
from serena.util.inspection import iter_subclasses
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerIdLike
|
||||
from solidlsp.util import subprocess_util
|
||||
from solidlsp.util.subprocess_util import terminate_process_tree_with_kill_fallback
|
||||
|
||||
@@ -1081,7 +1081,7 @@ class SerenaAgent:
|
||||
else:
|
||||
msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated.\n"
|
||||
if self._language_backend == LanguageBackend.LSP:
|
||||
language_servers_str = ", ".join([ls.value for ls in proj.project_config.language_servers])
|
||||
language_servers_str = ", ".join([ls.get_key() for ls in proj.project_config.language_servers])
|
||||
msg += f"Active language servers: {language_servers_str}.\n"
|
||||
msg += f"File encoding: {proj.project_config.encoding}.\n"
|
||||
|
||||
@@ -1436,7 +1436,7 @@ class SerenaAgent:
|
||||
"""
|
||||
self.get_active_project_or_raise().create_language_server_manager()
|
||||
|
||||
def add_language_server(self, ls_id: LanguageServerId) -> None:
|
||||
def add_language_server(self, ls_id: LanguageServerIdLike) -> None:
|
||||
"""
|
||||
Adds a new language server to the active project, spawning the respective language server and updating the project configuration.
|
||||
The addition is scheduled via the agent's task executor and executed synchronously, i.e. the method returns
|
||||
@@ -1444,16 +1444,16 @@ class SerenaAgent:
|
||||
|
||||
:param ls_id: the language server to add
|
||||
"""
|
||||
self.execute_task(lambda: self.get_active_project_or_raise().add_language_server(ls_id), name=f"AddLanguage:{ls_id.value}")
|
||||
self.execute_task(lambda: self.get_active_project_or_raise().add_language_server(ls_id), name=f"AddLanguage:{ls_id.get_key()}")
|
||||
|
||||
def remove_language_server(self, ls_id: LanguageServerId) -> None:
|
||||
def remove_language_server(self, ls_id: LanguageServerIdLike) -> None:
|
||||
"""
|
||||
Removes a language server from the active project, shutting down the respective server and updating the project configuration.
|
||||
The removal is scheduled via the agent's task executor and executed asynchronously.
|
||||
|
||||
:param ls_id: the language to remove
|
||||
"""
|
||||
self.issue_task(lambda: self.get_active_project_or_raise().remove_language_server(ls_id), name=f"RemoveLanguage:{ls_id.value}")
|
||||
self.issue_task(lambda: self.get_active_project_or_raise().remove_language_server(ls_id), name=f"RemoveLanguage:{ls_id.get_key()}")
|
||||
|
||||
def get_tool(self, tool_class: type[TTool]) -> TTool:
|
||||
return self._all_tools[tool_class]
|
||||
@@ -1498,7 +1498,7 @@ class SerenaAgent:
|
||||
tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
|
||||
return self.get_tool(tool_class)
|
||||
|
||||
def get_active_language_server_ids(self) -> list[LanguageServerId]:
|
||||
def get_active_language_server_ids(self) -> list[LanguageServerIdLike]:
|
||||
ls_manager = self.get_language_server_manager()
|
||||
if ls_manager is None:
|
||||
return []
|
||||
|
||||
+7
-5
@@ -40,7 +40,7 @@ from serena.prompt_factory import SerenaPromptFactory
|
||||
from serena.tools import ActivateProjectTool
|
||||
from serena.util.cli_util import AutoRegisteringGroup
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerId, LanguageServerIdLike
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
|
||||
@@ -722,7 +722,9 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
languages=languages if languages else None,
|
||||
interactive=True,
|
||||
)
|
||||
languages_str = ", ".join([lang.value for lang in generated_conf.language_servers]) if generated_conf.language_servers else "N/A"
|
||||
languages_str = (
|
||||
", ".join([lang.get_key() for lang in generated_conf.language_servers]) if generated_conf.language_servers else "N/A"
|
||||
)
|
||||
click.echo(f"Generated project with language servers {{{languages_str}}} at {yml_path}.")
|
||||
registered_project = serena_config.get_registered_project(str(project_root))
|
||||
if registered_project is None:
|
||||
@@ -813,7 +815,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
|
||||
collected_exceptions: list[Exception] = []
|
||||
files_failed = []
|
||||
language_file_counts: dict[LanguageServerId, int] = collections.defaultdict(lambda: 0)
|
||||
language_file_counts: dict[LanguageServerIdLike, int] = collections.defaultdict(lambda: 0)
|
||||
last_save_time = time.monotonic()
|
||||
for i, f in enumerate(tqdm(files, desc="Indexing")):
|
||||
try:
|
||||
@@ -828,7 +830,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
if now - last_save_time >= 30:
|
||||
ls_mgr.save_all_caches()
|
||||
last_save_time = now
|
||||
reported_language_file_counts = {k.value: v for k, v in language_file_counts.items()}
|
||||
reported_language_file_counts = {k.get_key(): v for k, v in language_file_counts.items()}
|
||||
click.echo(f"Indexed files per language: {dict_string(reported_language_file_counts, brackets=None)}")
|
||||
ls_mgr.save_all_caches()
|
||||
|
||||
@@ -894,7 +896,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
ls_mgr = proj.create_language_server_manager()
|
||||
try:
|
||||
ls = ls_mgr.get_language_server(file)
|
||||
click.echo(f"Indexing for language {ls.ls_id.value} …")
|
||||
click.echo(f"Indexing for language {ls.ls_id.get_key()} …")
|
||||
document_symbols = ls.request_document_symbols(file)
|
||||
symbols, _ = document_symbols.get_all_symbols_and_roots()
|
||||
if verbose:
|
||||
|
||||
@@ -35,7 +35,7 @@ from serena.constants import (
|
||||
from serena.util.inspection import compute_language_server_support_composition
|
||||
from serena.util.text_utils import GlobMatcher
|
||||
from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml_comments, save_yaml, transfer_yaml_comments
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerId, LanguageServerIdLike, LanguageServerRegistry
|
||||
|
||||
from ..analytics import RegisteredTokenCountEstimator
|
||||
from ..util.class_decorators import singleton
|
||||
@@ -321,7 +321,7 @@ class ProjectConfigAutoGenerationMode(Enum):
|
||||
@dataclass(kw_only=True)
|
||||
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
project_name: str
|
||||
language_servers: list[LanguageServerId]
|
||||
language_servers: list[LanguageServerIdLike]
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
ls_workspace_folders: list[str] = field(default_factory=lambda: ["."])
|
||||
ls_additional_workspace_folders: list[str] = field(default_factory=list)
|
||||
@@ -358,7 +358,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
@classmethod
|
||||
def _determine_project_language_servers(
|
||||
cls, project_root: str, interactive: bool, serena_config: "SerenaConfig"
|
||||
) -> list[LanguageServerId]:
|
||||
) -> list[LanguageServerIdLike]:
|
||||
log.info("Determining suitable language servers for the project")
|
||||
|
||||
# determine language servers to be considered and their priorities
|
||||
@@ -452,7 +452,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
determined_languages = cls._determine_project_language_servers(
|
||||
str(project_root), interactive=interactive, serena_config=serena_config
|
||||
)
|
||||
languages_to_use = [l.value for l in determined_languages]
|
||||
languages_to_use = [l.get_key() for l in determined_languages]
|
||||
else:
|
||||
languages_to_use = [lang.value for lang in languages]
|
||||
config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE)
|
||||
@@ -580,19 +580,14 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
"""
|
||||
# map languages to list of enum items, checking for errors
|
||||
lang_name_mapping = {"javascript": "typescript"}
|
||||
ls_ids: list[LanguageServerId] = []
|
||||
ls_ids: list[LanguageServerIdLike] = []
|
||||
ls_registry = LanguageServerRegistry.get_instance()
|
||||
for ls_str in data["language_servers"]:
|
||||
orig_language_str = ls_str
|
||||
try:
|
||||
ls_str = ls_str.lower()
|
||||
if ls_str in lang_name_mapping:
|
||||
ls_str = lang_name_mapping[ls_str]
|
||||
ls_id = LanguageServerId(ls_str)
|
||||
ls_ids.append(ls_id)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Invalid language server: '{orig_language_str}'.\nValid values are: {[l.value for l in LanguageServerId]}"
|
||||
) from e
|
||||
ls_str = ls_str.lower()
|
||||
if ls_str in lang_name_mapping:
|
||||
ls_str = lang_name_mapping[ls_str]
|
||||
ls_id = ls_registry.resolve(ls_str)
|
||||
ls_ids.append(ls_id)
|
||||
|
||||
# Validate activation_command_timeout
|
||||
activation_command_timeout_raw = data.get("activation_command_timeout", 180.0)
|
||||
@@ -669,7 +664,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
del d[k]
|
||||
|
||||
# map fields using non-primitive types to a YAML-compatible representation
|
||||
d["language_servers"] = [lang.value for lang in self.language_servers]
|
||||
d["language_servers"] = [lang.get_key() for lang in self.language_servers]
|
||||
d["language_backend"] = self.language_backend.value if self.language_backend is not None else None
|
||||
d["line_ending"] = self.line_ending.value if self.line_ending is not None else None
|
||||
|
||||
|
||||
+9
-20
@@ -29,6 +29,7 @@ from serena.util.logging import MemoryLogHandler
|
||||
from serena.util.pypi import PyPIPackageInfo
|
||||
from serena.util.pywebview import WebViewWithTray
|
||||
from serena.util.version import Version
|
||||
from solidlsp.ls_config import LanguageServerRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
@@ -513,7 +514,7 @@ class SerenaDashboardAPI:
|
||||
active_project_name = project.project_name if project else None
|
||||
project_info = {
|
||||
"name": active_project_name,
|
||||
"language": ", ".join([l.value for l in project.project_config.language_servers]) if project else None,
|
||||
"language": ", ".join([ls_id.get_key() for ls_id in project.project_config.language_servers]) if project else None,
|
||||
"path": str(project.project_root) if project else None,
|
||||
}
|
||||
|
||||
@@ -605,9 +606,9 @@ class SerenaDashboardAPI:
|
||||
available_memories = project.memory_manager.list_memories().get_full_list()
|
||||
|
||||
# Get list of languages for the active project
|
||||
languages = []
|
||||
ls_ids = []
|
||||
if project is not None:
|
||||
languages = [lang.value for lang in project.project_config.language_servers]
|
||||
ls_ids = [ls_id.get_key() for ls_id in project.project_config.language_servers]
|
||||
|
||||
# Get file encoding for the active project
|
||||
encoding = None
|
||||
@@ -626,7 +627,7 @@ class SerenaDashboardAPI:
|
||||
available_contexts=available_contexts,
|
||||
available_memories=available_memories,
|
||||
jetbrains_mode=self._agent.get_language_backend().is_jetbrains(),
|
||||
languages=languages,
|
||||
languages=ls_ids,
|
||||
encoding=encoding,
|
||||
current_client=Tool.get_last_tool_call_client_str(),
|
||||
serena_version=self._agent.version,
|
||||
@@ -637,15 +638,13 @@ class SerenaDashboardAPI:
|
||||
self._current_config_overview = self._compute_config_overview().model_dump()
|
||||
|
||||
def _get_available_languages(self) -> ResponseAvailableLanguages:
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
def run() -> ResponseAvailableLanguages:
|
||||
all_languages = [lang.value for lang in LanguageServerId.iter_all(include_experimental=True)]
|
||||
all_languages = LanguageServerRegistry.get_instance().get_keys()
|
||||
|
||||
# Filter out already added languages for the active project
|
||||
project = self._agent.get_active_project()
|
||||
if project:
|
||||
current_languages = [lang.value for lang in project.project_config.language_servers]
|
||||
current_languages = [ls_id.get_key() for ls_id in project.project_config.language_servers]
|
||||
available_languages = [lang for lang in all_languages if lang not in current_languages]
|
||||
else:
|
||||
available_languages = all_languages
|
||||
@@ -775,22 +774,12 @@ class SerenaDashboardAPI:
|
||||
return {}
|
||||
|
||||
def _add_language(self, request_add_language: RequestAddLanguage) -> None:
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
try:
|
||||
language = LanguageServerId(request_add_language.language)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid language server identifier: {request_add_language.language}")
|
||||
language = LanguageServerRegistry.get_instance().resolve(request_add_language.language)
|
||||
# add_language is already thread-safe
|
||||
self._agent.add_language_server(language)
|
||||
|
||||
def _remove_language(self, request_remove_language: RequestRemoveLanguage) -> None:
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
try:
|
||||
language = LanguageServerId(request_remove_language.language)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid language server identifier: {request_remove_language.language}")
|
||||
language = LanguageServerRegistry.get_instance().resolve(request_remove_language.language)
|
||||
# remove_language is already thread-safe
|
||||
self._agent.remove_language_server(language)
|
||||
|
||||
|
||||
+28
-26
@@ -9,7 +9,7 @@ from sensai.util.logging import LogTime
|
||||
|
||||
from serena.config.serena_config import ProjectConfig, SerenaPaths
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerConfig, LanguageServerIdLike
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeWatchedFilesParams, FileChangeType, FileEvent
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
@@ -45,7 +45,7 @@ class LanguageServerFactory:
|
||||
self.ls_specific_settings = ls_specific_settings
|
||||
self.trace_lsp_communication = trace_lsp_communication
|
||||
|
||||
def create_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
|
||||
def create_language_server(self, ls_id: LanguageServerIdLike) -> SolidLanguageServer:
|
||||
ls_config = LanguageServerConfig(
|
||||
workspace_folders=self.project_config.ls_workspace_folders,
|
||||
additional_workspace_folders=self.project_config.ls_additional_workspace_folders,
|
||||
@@ -75,7 +75,7 @@ class LanguageServerManager:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
language_servers: dict[LanguageServerId, SolidLanguageServer],
|
||||
language_servers: dict[LanguageServerIdLike, SolidLanguageServer],
|
||||
language_server_factory: LanguageServerFactory,
|
||||
project: "Project",
|
||||
) -> None:
|
||||
@@ -97,7 +97,9 @@ class LanguageServerManager:
|
||||
return next(iter(self._language_servers.values()))
|
||||
|
||||
@staticmethod
|
||||
def from_languages(languages: list[LanguageServerId], factory: LanguageServerFactory, project: "Project") -> "LanguageServerManager":
|
||||
def from_languages(
|
||||
languages: list[LanguageServerIdLike], factory: LanguageServerFactory, project: "Project"
|
||||
) -> "LanguageServerManager":
|
||||
"""
|
||||
Creates a manager with language servers for the given languages using the given factory.
|
||||
The language servers are started in parallel threads.
|
||||
@@ -109,21 +111,21 @@ class LanguageServerManager:
|
||||
"""
|
||||
|
||||
class StartLSThread(threading.Thread):
|
||||
def __init__(self, ls_id: LanguageServerId):
|
||||
super().__init__(target=self._start_language_server, name="StartLS:" + ls_id.value)
|
||||
def __init__(self, ls_id: LanguageServerIdLike):
|
||||
super().__init__(target=self._start_language_server, name="StartLS:" + ls_id.get_key())
|
||||
self.ls_id = ls_id
|
||||
self.language_server: SolidLanguageServer | None = None
|
||||
self.exception: Exception | None = None
|
||||
|
||||
def _start_language_server(self) -> None:
|
||||
try:
|
||||
with LogTime(f"Language server startup (language={self.ls_id.value})"):
|
||||
with LogTime(f"Language server startup (ls_id={self.ls_id.get_key()})"):
|
||||
self.language_server = factory.create_language_server(self.ls_id)
|
||||
self.language_server.start()
|
||||
if not self.language_server.is_running():
|
||||
raise RuntimeError(f"Failed to start the language server for language {self.ls_id.value}")
|
||||
raise RuntimeError(f"Failed to start the language server {self.ls_id.get_key()}")
|
||||
except Exception as e:
|
||||
log.error(f"Error starting language server for language {self.ls_id.value}: {e}", exc_info=e)
|
||||
log.error(f"Error starting language server {self.ls_id.get_key()}: {e}", exc_info=e)
|
||||
self.exception = e
|
||||
|
||||
# start language servers in parallel threads
|
||||
@@ -134,8 +136,8 @@ class LanguageServerManager:
|
||||
threads.append(thread)
|
||||
|
||||
# collect language servers and exceptions
|
||||
language_servers: dict[LanguageServerId, SolidLanguageServer] = {}
|
||||
exceptions: dict[LanguageServerId, Exception] = {}
|
||||
language_servers: dict[LanguageServerIdLike, SolidLanguageServer] = {}
|
||||
exceptions: dict[LanguageServerIdLike, Exception] = {}
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
if thread.exception is not None:
|
||||
@@ -154,7 +156,7 @@ class LanguageServerManager:
|
||||
if exceptions:
|
||||
for ls in language_servers.values():
|
||||
ls.stop()
|
||||
failure_messages = "\n".join([f"{lang.value}: {e}" for lang, e in exceptions.items()])
|
||||
failure_messages = "\n".join([f"{ls_id.get_key()}: {e}" for ls_id, e in exceptions.items()])
|
||||
raise LanguageServerManagerInitialisationError(f"Failed to start {len(exceptions)} language server(s):\n{failure_messages}")
|
||||
|
||||
return LanguageServerManager(language_servers, factory, project)
|
||||
@@ -183,7 +185,7 @@ class LanguageServerManager:
|
||||
ls = self._default_language_server
|
||||
return self._ensure_functional_ls(ls)
|
||||
|
||||
def _create_and_start_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
|
||||
def _create_and_start_language_server(self, ls_id: LanguageServerIdLike) -> SolidLanguageServer:
|
||||
if self._language_server_factory is None:
|
||||
raise ValueError(f"No language server factory available to create language server for {ls_id}")
|
||||
language_server = self._language_server_factory.create_language_server(ls_id)
|
||||
@@ -191,19 +193,19 @@ class LanguageServerManager:
|
||||
self._language_servers[ls_id] = language_server
|
||||
return language_server
|
||||
|
||||
def restart_language_server(self, language: LanguageServerId) -> SolidLanguageServer:
|
||||
def restart_language_server(self, ls_id: LanguageServerIdLike) -> SolidLanguageServer:
|
||||
"""
|
||||
Forces recreation and restart of the language server for the given language.
|
||||
It is assumed that the language server for the given language is no longer running.
|
||||
|
||||
:param language: the language
|
||||
:param ls_id: the language server identifier
|
||||
:return: the newly created language server
|
||||
"""
|
||||
if language not in self._language_servers:
|
||||
raise ValueError(f"No language server for language {language.value} present; cannot restart")
|
||||
return self._create_and_start_language_server(language)
|
||||
if ls_id not in self._language_servers:
|
||||
raise ValueError(f"No language server for language {ls_id.get_key()} present; cannot restart")
|
||||
return self._create_and_start_language_server(ls_id)
|
||||
|
||||
def add_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
|
||||
def add_language_server(self, ls_id: LanguageServerIdLike) -> SolidLanguageServer:
|
||||
"""
|
||||
Dynamically adds a new language server for the given language.
|
||||
|
||||
@@ -211,21 +213,21 @@ class LanguageServerManager:
|
||||
:return: the newly created language server
|
||||
"""
|
||||
if ls_id in self._language_servers:
|
||||
raise ValueError(f"Language server for language {ls_id.value} already present")
|
||||
raise ValueError(f"Language server {ls_id.get_key()} already present")
|
||||
return self._create_and_start_language_server(ls_id)
|
||||
|
||||
def remove_language_server(self, language: LanguageServerId, save_cache: bool = False) -> None:
|
||||
def remove_language_server(self, ls_id: LanguageServerIdLike, save_cache: bool = False) -> None:
|
||||
"""
|
||||
Removes the language server for the given language, stopping it if it is running.
|
||||
|
||||
:param language: the language
|
||||
:param ls_id: the language
|
||||
"""
|
||||
if language not in self._language_servers:
|
||||
raise ValueError(f"No language server for language {language.value} present; cannot remove")
|
||||
ls = self._language_servers.pop(language)
|
||||
if ls_id not in self._language_servers:
|
||||
raise ValueError(f"No language server for language {ls_id.get_key()} present; cannot remove")
|
||||
ls = self._language_servers.pop(ls_id)
|
||||
self._stop_language_server(ls, save_cache=save_cache)
|
||||
|
||||
def get_active_language_server_ids(self) -> list[LanguageServerId]:
|
||||
def get_active_language_server_ids(self) -> list[LanguageServerIdLike]:
|
||||
"""
|
||||
Returns the list of languages for which language servers are currently managed.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from serena.util.file_proxy import FileCollection, FileProxy
|
||||
from serena.util.file_system import GitignoreParser, match_path, scan_directory
|
||||
from serena.util.text_utils import MatchedConsecutiveLines, search_files
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerIdLike
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
@@ -567,7 +567,7 @@ class Project(ToStringMixin):
|
||||
raise Exception(msg.build())
|
||||
return self.language_server_manager
|
||||
|
||||
def add_language_server(self, ls_id: LanguageServerId) -> None:
|
||||
def add_language_server(self, ls_id: LanguageServerIdLike) -> None:
|
||||
"""
|
||||
Adds a new language server to the project configuration, starting the corresponding
|
||||
server instance if the LS manager is active.
|
||||
@@ -576,21 +576,21 @@ class Project(ToStringMixin):
|
||||
:param ls_id: the language server to add
|
||||
"""
|
||||
if ls_id in self.project_config.language_servers:
|
||||
log.info(f"Language server {ls_id.value} is already present in the project configuration.")
|
||||
log.info(f"Language server {ls_id.get_key()} is already present in the project configuration.")
|
||||
return
|
||||
|
||||
# start the language server (if the LS manager is active)
|
||||
if self.language_server_manager is None:
|
||||
log.info("Language server manager is not active; skipping language server startup for the new language.")
|
||||
else:
|
||||
log.info("Adding and starting the language server '%s' ...", ls_id.value)
|
||||
log.info("Adding and starting the language server '%s' ...", ls_id.get_key())
|
||||
self.language_server_manager.add_language_server(ls_id)
|
||||
|
||||
# update the project configuration
|
||||
self.project_config.language_servers.append(ls_id)
|
||||
self.save_config()
|
||||
|
||||
def remove_language_server(self, ls_id: LanguageServerId) -> None:
|
||||
def remove_language_server(self, ls_id: LanguageServerIdLike) -> None:
|
||||
"""
|
||||
Removes a language server from the project configuration, stopping the corresponding
|
||||
server instance if the LS manager is active.
|
||||
@@ -599,7 +599,7 @@ class Project(ToStringMixin):
|
||||
:param ls_id: the language server to remove
|
||||
"""
|
||||
if ls_id not in self.project_config.language_servers:
|
||||
log.info(f"Language {ls_id.value} is not present in the project configuration.")
|
||||
log.info(f"Language {ls_id.get_key()} is not present in the project configuration.")
|
||||
return
|
||||
# update the project configuration
|
||||
self.project_config.language_servers.remove(ls_id)
|
||||
@@ -609,7 +609,7 @@ class Project(ToStringMixin):
|
||||
if self.language_server_manager is None:
|
||||
log.info("Language server manager is not active; skipping language server shutdown for the removed language.")
|
||||
else:
|
||||
log.info("Removing and stopping the language server for language %s ...", ls_id.value)
|
||||
log.info("Removing and stopping the language server for language %s ...", ls_id.get_key())
|
||||
self.language_server_manager.remove_language_server(ls_id)
|
||||
|
||||
def ls_sync_file_system_changes(self) -> int:
|
||||
|
||||
@@ -29,6 +29,8 @@ project_name: "project_name"
|
||||
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# - External Python adapters may add further registered IDs; install the adapter package first
|
||||
# and then use its ID here, for example: example
|
||||
# Special requirements:
|
||||
# Some language servers require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
|
||||
@@ -95,11 +95,6 @@ class AngularTypeScriptServer(TypeScriptLanguageServer):
|
||||
cross-file references that span Angular templates.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def get_language_server_id(cls) -> LanguageServerId:
|
||||
return LanguageServerId.TYPESCRIPT
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
# Use the Angular matcher so .html template files aren't filtered out of
|
||||
# reference / search results when the companion is asked about them.
|
||||
|
||||
@@ -532,7 +532,7 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
config,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=scala_lsp_executable_path, cwd=repository_root_path),
|
||||
config.ls_id.value,
|
||||
"scala",
|
||||
solidlsp_settings,
|
||||
)
|
||||
|
||||
|
||||
@@ -98,12 +98,6 @@ class SvelteTypeScriptServer(TypeScriptLanguageServer):
|
||||
self._explicit_ts_ls_executable = ts_ls_executable_path
|
||||
super().__init__(config, repository_root_path, solidlsp_settings)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def get_language_server_id(cls) -> LanguageServerId:
|
||||
"""Return TYPESCRIPT; companion uses the TypeScript LS infrastructure."""
|
||||
return LanguageServerId.TYPESCRIPT
|
||||
|
||||
@override
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
# include .svelte so references returned by the plugin are not filtered out
|
||||
|
||||
@@ -37,17 +37,6 @@ log = logging.getLogger(__name__)
|
||||
class VueTypeScriptServer(TypeScriptLanguageServer):
|
||||
"""TypeScript LS configured with @vue/typescript-plugin for Vue file support."""
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def get_language_server_id(cls) -> LanguageServerId:
|
||||
"""Return TYPESCRIPT since this is a TypeScript language server variant.
|
||||
|
||||
Note: VueTypeScriptServer is a companion server that uses TypeScript's language server
|
||||
with the Vue TypeScript plugin. It reports as TYPESCRIPT to maintain compatibility
|
||||
with the TypeScript language server infrastructure.
|
||||
"""
|
||||
return LanguageServerId.TYPESCRIPT
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
# must override with Vue-specific matcher to ensure .vue files are included (as they can be discovered via references,
|
||||
# for instance; otherwise, we may find references in .vue files but then filter the results out, because .vue files are ignored.)
|
||||
|
||||
+3
-7
@@ -31,7 +31,7 @@ from solidlsp.dependency_provider import (
|
||||
LanguageServerDependencyProviderUvx,
|
||||
)
|
||||
from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder
|
||||
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
|
||||
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import InvalidTextLocationError, SolidLSPException
|
||||
from solidlsp.ls_process import DEFAULT_LS_REQUEST_TIMEOUT, LanguageServerInterface, StdioLanguageServer
|
||||
from solidlsp.ls_types import UnifiedSymbolInformation
|
||||
@@ -415,10 +415,6 @@ class SolidLanguageServer(ABC):
|
||||
else:
|
||||
return logging.INFO
|
||||
|
||||
@classmethod
|
||||
def get_language_server_id(cls) -> LanguageServerId:
|
||||
return LanguageServerId.from_ls_class(cls)
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
"""
|
||||
@@ -514,7 +510,7 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
self.config = config
|
||||
self._solidlsp_settings = solidlsp_settings
|
||||
ls_id = self.get_language_server_id()
|
||||
ls_id = config.ls_id
|
||||
self._custom_settings = solidlsp_settings.get_ls_specific_settings(ls_id)
|
||||
"""
|
||||
the (user-provided) language server-specific settings
|
||||
@@ -533,7 +529,7 @@ class SolidLanguageServer(ABC):
|
||||
default language identifier to be passed to the language server in `textDocument/didOpen` notifications.
|
||||
"""
|
||||
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
|
||||
self.ls_id = self.get_language_server_id()
|
||||
self.ls_id = ls_id
|
||||
"""
|
||||
identifies the language server (not to be confused with the language_id passed to the language server)
|
||||
"""
|
||||
|
||||
+145
-15
@@ -2,15 +2,19 @@
|
||||
Configuration objects for language servers
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Self
|
||||
from typing import TYPE_CHECKING, Protocol, Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from solidlsp import SolidLanguageServer
|
||||
@@ -87,9 +91,34 @@ class FilenameMatcher:
|
||||
return False
|
||||
|
||||
|
||||
class LanguageServerId(str, Enum):
|
||||
class LanguageServerIdLike(Protocol):
|
||||
"""
|
||||
Enumeration of language servers supported by SolidLSP.
|
||||
Protocol defining the interface for language server identifiers,
|
||||
which can be either a :class:`LanguageServerId` enum member or any other object that implements the same methods.
|
||||
For language server adapters defined outside the SolidLSP library, use :class:`ExternalLanguageServerId`.
|
||||
"""
|
||||
|
||||
def get_key(self) -> str:
|
||||
"""
|
||||
:return: the string key which identifies this language server in configuration files
|
||||
"""
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
pass
|
||||
|
||||
def get_ls_class(self) -> type["SolidLanguageServer"]:
|
||||
pass
|
||||
|
||||
def get_priority(self) -> int:
|
||||
pass
|
||||
|
||||
def supports_implementation_request(self) -> bool:
|
||||
return self.get_ls_class().supports_implementation_request()
|
||||
|
||||
|
||||
class LanguageServerId(Enum):
|
||||
"""
|
||||
Enumeration of language servers directly supported by SolidLSP.
|
||||
"""
|
||||
|
||||
CSHARP = "csharp"
|
||||
@@ -320,6 +349,9 @@ class LanguageServerId(str, Enum):
|
||||
if include_non_programming_languages or lang.is_programming_language():
|
||||
yield lang
|
||||
|
||||
def get_key(self) -> str:
|
||||
return self.value
|
||||
|
||||
def is_experimental(self) -> bool:
|
||||
"""
|
||||
Check if the language server is experimental (potentially not robust),
|
||||
@@ -938,19 +970,117 @@ class LanguageServerId(str, Enum):
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
@classmethod
|
||||
def from_ls_class(cls, ls_class: type["SolidLanguageServer"]) -> Self:
|
||||
"""
|
||||
Get the Language enum value from a SolidLanguageServer class.
|
||||
|
||||
:param ls_class: The SolidLanguageServer class to find the corresponding Language for
|
||||
:return: The Language enum value
|
||||
:raises ValueError: If the language server class is not supported
|
||||
class ExternalLanguageServerId(LanguageServerIdLike):
|
||||
"""
|
||||
Represents an identifier for a language server that is externally provided and registered.
|
||||
"""
|
||||
|
||||
def __init__(self, key: str, matcher: FilenameMatcher, implementation: type["SolidLanguageServer"], priority: int = 0):
|
||||
self._key = key
|
||||
self._matcher = matcher
|
||||
self._implementation = implementation
|
||||
self._priority = priority
|
||||
|
||||
def get_key(self) -> str:
|
||||
return self._key
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
return self._matcher
|
||||
|
||||
def get_ls_class(self) -> type["SolidLanguageServer"]:
|
||||
return self._implementation
|
||||
|
||||
def get_priority(self) -> int:
|
||||
return self._priority
|
||||
|
||||
|
||||
class LanguageServerRegistry:
|
||||
"""
|
||||
Registry of language servers
|
||||
"""
|
||||
|
||||
REGISTRATION_ENTRY_POINT_GROUP = "solidlsp.language_server_registration"
|
||||
"""
|
||||
entry point group for language server registration functions; each function should call use
|
||||
`LanguageServerRegistry.get_instance().register(...)` to register a language server
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
with cls._instance_lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = cls(True)
|
||||
cls._discover_language_server_registrations_from_entry_points()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, _singleton: bool):
|
||||
if not _singleton:
|
||||
raise RuntimeError("LanguageServerRegistry is a singleton. Use get_instance() to access it.")
|
||||
self._registered_language_servers: dict[str, LanguageServerIdLike] = {}
|
||||
|
||||
# auto-register built-in language servers
|
||||
for ls_id in LanguageServerId:
|
||||
self._registered_language_servers[ls_id.get_key()] = ls_id
|
||||
|
||||
@classmethod
|
||||
def _discover_language_server_registrations_from_entry_points(cls) -> None:
|
||||
"""
|
||||
for enum_instance in cls:
|
||||
if enum_instance.get_ls_class() == ls_class:
|
||||
return enum_instance
|
||||
raise ValueError(f"Unhandled language server class: {ls_class}")
|
||||
Discover and execute language server adapter registration functions from entry points.
|
||||
"""
|
||||
log.debug("Discovering language server registration entry points ...")
|
||||
try:
|
||||
entry_points = importlib.metadata.entry_points(group=cls.REGISTRATION_ENTRY_POINT_GROUP)
|
||||
except Exception as error:
|
||||
log.exception("Failed to discover language server registration entry points: %s", error)
|
||||
return
|
||||
|
||||
def get_distribution_name(ep: importlib.metadata.EntryPoint) -> str:
|
||||
distribution = getattr(ep, "dist", None)
|
||||
if distribution is None:
|
||||
return "unknown distribution"
|
||||
return distribution.name or "unknown distribution"
|
||||
|
||||
log.debug("Found %d language server registration entry points", len(entry_points))
|
||||
for entry_point in entry_points:
|
||||
try:
|
||||
registration = entry_point.load()
|
||||
if not callable(registration):
|
||||
raise TypeError("entry point must resolve to a callable registration function")
|
||||
registration()
|
||||
except Exception as error:
|
||||
log.exception(
|
||||
"Failed to load language server adapter entry point '%s' from %s: %s",
|
||||
entry_point.name,
|
||||
get_distribution_name(entry_point),
|
||||
error,
|
||||
)
|
||||
|
||||
def resolve(self, key: str) -> LanguageServerIdLike:
|
||||
if key in self._registered_language_servers:
|
||||
return self._registered_language_servers[key]
|
||||
raise ValueError(f"Unknown language server key: '{key}'; Valid keys: {self.get_keys()}")
|
||||
|
||||
def register(self, ls_id: LanguageServerIdLike, allow_override: bool = False) -> None:
|
||||
"""
|
||||
:param ls_id: the identifier to register
|
||||
:param allow_override: whether to allow overriding an existing registration with the same key
|
||||
"""
|
||||
log.info("Registering language server: %s (class=%s)", ls_id.get_key(), ls_id.get_ls_class().__name__)
|
||||
key = ls_id.get_key()
|
||||
if key in self._registered_language_servers and not allow_override:
|
||||
raise ValueError(f"Language server id is already registered: {key}")
|
||||
self._registered_language_servers[key] = ls_id
|
||||
|
||||
def get_keys(self) -> list[str]:
|
||||
"""
|
||||
:return: the sorted list of all registered string keys
|
||||
"""
|
||||
return sorted(self._registered_language_servers.keys())
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -959,7 +1089,7 @@ class LanguageServerConfig:
|
||||
Configuration parameters for a language server instance
|
||||
"""
|
||||
|
||||
ls_id: LanguageServerId
|
||||
ls_id: LanguageServerIdLike
|
||||
"""
|
||||
defines the language server to use
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
This module contains the exceptions raised by the framework.
|
||||
"""
|
||||
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerIdLike
|
||||
|
||||
|
||||
class SolidLSPException(Exception):
|
||||
@@ -28,7 +28,7 @@ class SolidLSPException(Exception):
|
||||
|
||||
return isinstance(self.cause, LanguageServerTerminatedException)
|
||||
|
||||
def get_affected_language(self) -> LanguageServerId | None:
|
||||
def get_affected_language(self) -> LanguageServerIdLike | None:
|
||||
"""
|
||||
:return: the affected language for the case where the exception is caused by the language server having terminated
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import IO, Any, AnyStr
|
||||
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerIdLike
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_request import LanguageServerRequest
|
||||
from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification
|
||||
@@ -58,7 +58,7 @@ class LanguageServerTerminatedException(Exception):
|
||||
Exception raised when the language server process has terminated unexpectedly.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, ls_id: LanguageServerId, cause: Exception | None = None) -> None:
|
||||
def __init__(self, message: str, ls_id: LanguageServerIdLike, cause: Exception | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.ls_id = ls_id
|
||||
@@ -121,7 +121,7 @@ class LanguageServerInterface(ABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ls_id: LanguageServerId,
|
||||
ls_id: LanguageServerIdLike,
|
||||
determine_log_level: Callable[[str], int],
|
||||
logger: Callable[[str, str, StringDict | str], None] | None = None,
|
||||
request_timeout: float | None = None,
|
||||
@@ -491,7 +491,7 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
def __init__(
|
||||
self,
|
||||
process_launch_info: ProcessLaunchInfo,
|
||||
ls_id: LanguageServerId,
|
||||
ls_id: LanguageServerIdLike,
|
||||
determine_log_level: Callable[[str], int],
|
||||
logger: Callable[[str, str, StringDict | str], None] | None = None,
|
||||
start_independent_lsp_process: bool = True,
|
||||
@@ -519,7 +519,7 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
log.info("Starting language server process via command: %s", self._process_launch_info.cmd)
|
||||
|
||||
process = subprocess_util.ManagedSubprocessLauncher.get_instance().launch(
|
||||
self._process_launch_info, name=f"LS[{self.ls_id.value}]", start_new_session=self._start_independent_lsp_process
|
||||
self._process_launch_info, name=f"LS[{self.ls_id.get_key()}]", start_new_session=self._start_independent_lsp_process
|
||||
)
|
||||
self._process = process
|
||||
|
||||
@@ -534,12 +534,12 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
# start threads to read stdout and stderr of the process
|
||||
threading.Thread(
|
||||
target=self._read_ls_process_stdout,
|
||||
name=f"LSP-stdout-reader:{self.ls_id.value}",
|
||||
name=f"LSP-stdout-reader:{self.ls_id.get_key()}",
|
||||
daemon=True,
|
||||
).start()
|
||||
threading.Thread(
|
||||
target=self._read_ls_process_stderr,
|
||||
name=f"LSP-stderr-reader:{self.ls_id.value}",
|
||||
name=f"LSP-stderr-reader:{self.ls_id.get_key()}",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
@@ -689,7 +689,7 @@ class TCPLanguageServer(LanguageServerInterface):
|
||||
def __init__(
|
||||
self,
|
||||
connection_info: TCPConnectionInfo,
|
||||
ls_id: LanguageServerId,
|
||||
ls_id: LanguageServerIdLike,
|
||||
determine_log_level: Callable[[str], int],
|
||||
logger: Callable[[str, str, StringDict | str], None] | None = None,
|
||||
request_timeout: float | None = None,
|
||||
@@ -732,7 +732,7 @@ class TCPLanguageServer(LanguageServerInterface):
|
||||
|
||||
threading.Thread(
|
||||
target=self._read_loop,
|
||||
name=f"LSP-tcp-reader:{self.ls_id.value}",
|
||||
name=f"LSP-tcp-reader:{self.ls_id.get_key()}",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from sensai.util.helper import count_not_none
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerId, LanguageServerIdLike
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,11 +38,12 @@ class SolidLSPSettings:
|
||||
For instance, if this is "/home/user/myproject/.solidlsp",
|
||||
then Solid-LSP will store project-specific data (e.g. caches) in that directory.
|
||||
"""
|
||||
ls_specific_settings: dict["LanguageServerId", dict[str, Any]] = field(default_factory=dict)
|
||||
ls_specific_settings: dict[Union[str, "LanguageServerId"], dict[str, Any]] = field(default_factory=dict)
|
||||
"""
|
||||
Advanced configuration option allowing to configure language server implementation specific options.
|
||||
Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available.
|
||||
No documentation on options means no options are available.
|
||||
Maps language server identifiers or their string keys to a dictionary of options for that language server.
|
||||
See constructors of the corresponding LS implementations within SolidLSP to see which options are available.
|
||||
Some options are centrally supported. See Serena's documentation for details.
|
||||
"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -76,11 +78,19 @@ class SolidLSPSettings:
|
||||
value = default_value
|
||||
return value
|
||||
|
||||
def get_ls_specific_settings(self, ls_id: "LanguageServerId") -> CustomLSSettings:
|
||||
def get_ls_specific_settings(self, ls_id: "LanguageServerIdLike") -> CustomLSSettings:
|
||||
"""
|
||||
Gets the custom settings for the given language server
|
||||
|
||||
:param ls_id: the language server identifier for which to retrieve settings
|
||||
:return: a dictionary of settings for the language server
|
||||
"""
|
||||
return self.CustomLSSettings(self.ls_specific_settings.get(ls_id))
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
settings_dict = self.ls_specific_settings.get(ls_id.get_key())
|
||||
if isinstance(ls_id, LanguageServerId):
|
||||
settings_dict2 = self.ls_specific_settings.get(ls_id)
|
||||
if count_not_none(settings_dict, settings_dict2) > 1:
|
||||
raise ValueError(f"Duplicate LS-specific settings for {ls_id.get_key()}: Found keys {ls_id.get_key()} and {ls_id}")
|
||||
settings_dict = settings_dict or settings_dict2
|
||||
return self.CustomLSSettings(settings_dict)
|
||||
+3
-3
@@ -65,8 +65,8 @@ PYTHON_LANGUAGE_BACKENDS = [LanguageServerId.PYTHON, LanguageServerId.PYTHON_TY,
|
||||
|
||||
|
||||
def get_repo_path(language: LanguageServerId) -> Path:
|
||||
repo_language = _LANGUAGE_REPO_ALIASES.get(language, language)
|
||||
return Path(__file__).parent / "resources" / "repos" / repo_language / "test_repo"
|
||||
ls_id = _LANGUAGE_REPO_ALIASES.get(language, language)
|
||||
return Path(__file__).parent / "resources" / "repos" / ls_id.get_key() / "test_repo"
|
||||
|
||||
|
||||
def _create_ls(
|
||||
@@ -325,7 +325,7 @@ def get_pytest_markers(ls_id: LanguageServerId) -> list[MarkDecorator | Mark]:
|
||||
"""
|
||||
return [
|
||||
*_LANGUAGE_PYTEST_MARKERS[ls_id],
|
||||
pytest.mark.skipif(not language_server_tests_enabled(ls_id), reason=f"{ls_id.value} tests are disabled in this environment"),
|
||||
pytest.mark.skipif(not language_server_tests_enabled(ls_id), reason=f"{ls_id.get_key()} tests are disabled in this environment"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ def test_bsl_dependency_provider_custom_version_no_sha() -> None:
|
||||
from solidlsp.language_servers.common import RuntimeDependencyCollection
|
||||
|
||||
settings = SolidLSPSettings()
|
||||
settings.ls_specific_settings[LanguageServerId.BSL] = {"bsl_ls_version": "0.28.0"}
|
||||
settings.ls_specific_settings[LanguageServerId.BSL.get_key()] = {"bsl_ls_version": "0.28.0"}
|
||||
provider = BSLLanguageServer.DependencyProvider(
|
||||
settings.get_ls_specific_settings(LanguageServerId.BSL),
|
||||
"/tmp/ls_resources",
|
||||
@@ -216,7 +216,7 @@ def test_bsl_launch_command_uses_ls_path_without_download() -> None:
|
||||
from solidlsp.language_servers.bsl_language_server import BSLLanguageServer
|
||||
|
||||
settings = SolidLSPSettings()
|
||||
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
settings.ls_specific_settings[LanguageServerId.BSL.get_key()] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
provider = BSLLanguageServer.DependencyProvider(
|
||||
settings.get_ls_specific_settings(LanguageServerId.BSL),
|
||||
"/tmp/ls_resources",
|
||||
@@ -243,7 +243,7 @@ def test_bsl_launch_command_requires_java() -> None:
|
||||
from solidlsp.language_servers.bsl_language_server import BSLLanguageServer
|
||||
|
||||
settings = SolidLSPSettings()
|
||||
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
settings.ls_specific_settings[LanguageServerId.BSL.get_key()] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
provider = BSLLanguageServer.DependencyProvider(
|
||||
settings.get_ls_specific_settings(LanguageServerId.BSL),
|
||||
"/tmp/ls_resources",
|
||||
@@ -260,7 +260,7 @@ def test_bsl_launch_command_rejects_old_java() -> None:
|
||||
from solidlsp.language_servers.bsl_language_server import BSL_LS_MIN_JAVA_VERSION, BSLLanguageServer
|
||||
|
||||
settings = SolidLSPSettings()
|
||||
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
settings.ls_specific_settings[LanguageServerId.BSL.get_key()] = {"ls_path": "/custom/path/bsl-language-server.jar"}
|
||||
provider = BSLLanguageServer.DependencyProvider(
|
||||
settings.get_ls_specific_settings(LanguageServerId.BSL),
|
||||
"/tmp/ls_resources",
|
||||
|
||||
@@ -92,11 +92,6 @@ class TestRLanguageServer:
|
||||
assert not matcher.is_relevant_filename("script.py")
|
||||
assert not matcher.is_relevant_filename("README.md")
|
||||
|
||||
def test_r_language_enum(self):
|
||||
"""Test R language enum value."""
|
||||
assert LanguageServerId.R == "r"
|
||||
assert str(LanguageServerId.R) == "r"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.R], indirect=True)
|
||||
def test_bare_symbol_names(self, language_server) -> None:
|
||||
all_symbols = request_all_symbols(language_server)
|
||||
|
||||
Reference in new issue
Block a user