mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 00:15:05 +00:00
Bridge external language server adapters through the registry (#2089)
* Bridge external language server adapters through the registry The plugin mechanism (ExternalLanguageServerId + LanguageServerRegistry via solidlsp.language_server_registration entry points) was added but three places that consume language IDs still only iterate the built-in LanguageServerId enum, making externally-registered adapters unreachable from project.yml and the CLI. This change: - _determine_project_language_servers: also scans externally- registered LSes for auto-detection. - serena project create --ls: accepts registry keys as fallback. - No behavior change for built-in language servers. Motivation: enables first-class support for language servers like vhdl_ls that ship as separate packages rather than being vendored into the main repo. * Widen signatures for external language server support Follow-up to fix/external-ls-registry: - compute_language_server_support_composition: accept and return LanguageServerIdLike instead of LanguageServerId enum members only, since externally-registered adapters are also LanguageServerIdLike. - ProjectConfig.autogenerate: same widening for the languages parameter. - Two string-formatting sites in _determine_project_language_servers use .get_key() instead of .value so they work for either type. Type checker (ty) clean on src/serena and src/solidlsp; pre-existing diagnostics unrelated to this PR remain. * Refine based on review: registry as SoT, widen get_ls_priority Following @opcode81's review feedback: - Add LanguageServerRegistry.iter_registered_ls_ids() — a single iterator that yields all registered LSes (built-in enum + externals). - _determine_project_language_servers uses the new iterator instead of the split 'enum + dedup-externals' loop. - get_ls_priority now accepts LanguageServerIdLike, so user-configured priorities via serena_config.ls_priorities apply to external LSes too. Internally uses ls.get_key() (defined on both enum and ExternalLanguageServerId). - project create --ls drops the try/except fallback: resolves via registry directly. Unknown keys now report the full registry key list in the error message. Net diff vs previous attempt: +21/-27 vs +33/-14 (simpler). * Bridge external language server adapters through the registry
This commit is contained in:
1 parent
9f19a79a04
commit
dc97aba74a
4 files changed
+35
-22
No files matched your search
+6
-5
@@ -43,7 +43,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, LanguageServerIdLike
|
||||
from solidlsp.ls_config import LanguageServerIdLike, LanguageServerRegistry
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
|
||||
@@ -720,14 +720,15 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
if os.path.exists(yml_path):
|
||||
raise FileExistsError(f"Project file {yml_path} already exists.")
|
||||
|
||||
languages: list[LanguageServerId] = []
|
||||
languages: list[LanguageServerIdLike] = []
|
||||
if language:
|
||||
registry = LanguageServerRegistry.get_instance()
|
||||
for lang in language:
|
||||
ls_key = lang.lower()
|
||||
try:
|
||||
languages.append(LanguageServerId(lang.lower()))
|
||||
languages.append(registry.resolve(ls_key))
|
||||
except ValueError:
|
||||
all_langs = [l.value for l in LanguageServerId]
|
||||
raise ValueError(f"Unknown language '{lang}'. Supported: {all_langs}")
|
||||
raise ValueError(f"Unknown language '{lang}'. Supported: {registry.get_keys()}")
|
||||
|
||||
generated_conf = ProjectConfig.autogenerate(
|
||||
project_root=project_path,
|
||||
|
||||
@@ -37,7 +37,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, LanguageServerIdLike, LanguageServerRegistry
|
||||
from solidlsp.ls_config import LanguageServerIdLike, LanguageServerRegistry
|
||||
|
||||
from ..analytics import RegisteredTokenCountEstimator
|
||||
from ..language_backend import BuiltinLanguageBackend, LanguageBackend, LanguageBackendRegistry
|
||||
@@ -368,11 +368,15 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
log.info("Determining suitable language servers for the project")
|
||||
|
||||
# determine language servers to be considered and their priorities
|
||||
ls_priorities = {}
|
||||
for language in LanguageServerId:
|
||||
priority = serena_config.get_ls_priority(language)
|
||||
# the registry is the single source of truth — it includes both built-in enum members
|
||||
# and externally-registered adapters (via solidlsp.language_server_registration entry points).
|
||||
# priorities are user-configurable per-key via serena_config.ls_priorities (works for both kinds).
|
||||
ls_priorities: dict[LanguageServerIdLike, int] = {}
|
||||
registry = LanguageServerRegistry.get_instance()
|
||||
for ls_id in registry.iter_registered_ls_ids():
|
||||
priority = serena_config.get_ls_priority(ls_id)
|
||||
if priority > 0:
|
||||
ls_priorities[language] = priority
|
||||
ls_priorities[ls_id] = priority
|
||||
|
||||
log.debug("Language server priorities: %s", ls_priorities)
|
||||
ls_composition = compute_language_server_support_composition(project_root, list(ls_priorities.keys()))
|
||||
@@ -399,7 +403,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
if len(other_language_pairs) > 0 and interactive:
|
||||
print(
|
||||
"Detected and enabled main language server '%s' (%.2f%% of source files)."
|
||||
% (top_language_pair[0].value, top_language_pair[1])
|
||||
% (top_language_pair[0].get_key(), top_language_pair[1])
|
||||
)
|
||||
print(f"Additionally detected {len(other_language_pairs)} other applicable language servers.\n")
|
||||
print("Note: Enable only servers for languages you need symbolic retrieval/editing capabilities for.")
|
||||
@@ -407,7 +411,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
print(" system-level installations/configuration (see Serena documentation).")
|
||||
print("\nWhich additional language servers do you want to enable?")
|
||||
for ls_id, perc in other_language_pairs:
|
||||
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (ls_id.value, perc), default=False)
|
||||
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (ls_id.get_key(), perc), default=False)
|
||||
if enable:
|
||||
language_servers_to_use.append(ls_id)
|
||||
print()
|
||||
@@ -421,7 +425,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
project_root: str | Path,
|
||||
serena_config: "SerenaConfig",
|
||||
project_name: str | None = None,
|
||||
languages: list[LanguageServerId] | None = None,
|
||||
languages: list[LanguageServerIdLike] | None = None,
|
||||
save_to_disk: bool = True,
|
||||
interactive: bool = False,
|
||||
asynchronous: bool = False,
|
||||
@@ -460,7 +464,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
)
|
||||
languages_to_use = [l.get_key() for l in determined_languages]
|
||||
else:
|
||||
languages_to_use = [lang.value for lang in languages]
|
||||
languages_to_use = [lang.get_key() for lang in languages]
|
||||
config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE)
|
||||
config_with_comments["project_name"] = project_name
|
||||
config_with_comments["language_servers"] = languages_to_use
|
||||
@@ -1564,7 +1568,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
log.info(f"Using language backend from global configuration: {language_backend.name}")
|
||||
return language_backend
|
||||
|
||||
def get_ls_priority(self, ls_id: LanguageServerId) -> int:
|
||||
def get_ls_priority(self, ls_id: LanguageServerIdLike) -> int:
|
||||
"""
|
||||
Gets the priority value associated with a language server
|
||||
|
||||
@@ -1573,9 +1577,9 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
"""
|
||||
if self.ls_priorities is not None:
|
||||
try:
|
||||
configured_value = self.ls_priorities.get(ls_id.value)
|
||||
configured_value = self.ls_priorities.get(ls_id.get_key())
|
||||
if configured_value is not None:
|
||||
return int(configured_value)
|
||||
except Exception as e:
|
||||
log.error("Error reading language priority for %s: %s. Using default priority.", ls_id.value, e)
|
||||
log.error("Error reading language priority for %s: %s. Using default priority.", ls_id.get_key(), e)
|
||||
return ls_id.get_priority()
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Callable, Iterator
|
||||
from typing import TypeVar
|
||||
|
||||
from serena.util.file_system import find_all_non_ignored_files
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_config import LanguageServerId, LanguageServerIdLike
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -38,8 +38,8 @@ def iter_subclasses(
|
||||
|
||||
|
||||
def compute_language_server_support_composition(
|
||||
repo_path: str, ls_ids: list[LanguageServerId] | None = None
|
||||
) -> dict[LanguageServerId, float]:
|
||||
repo_path: str, ls_ids: list[LanguageServerIdLike] | None = None
|
||||
) -> dict[LanguageServerIdLike, float]:
|
||||
"""
|
||||
Determine the composition of a repository in terms of the language servers that can be used to analyze it.
|
||||
|
||||
@@ -64,7 +64,7 @@ def compute_language_server_support_composition(
|
||||
matchers = {lang: lang.get_source_fn_matcher() for lang in ls_ids}
|
||||
|
||||
# count files per language in a single pass over the files
|
||||
ls_file_counts: dict[LanguageServerId, int] = {}
|
||||
ls_file_counts: dict[LanguageServerIdLike, int] = {}
|
||||
recognised_files = 0
|
||||
for file_path in all_files:
|
||||
# Use just the filename for matching, not the full path
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import cache
|
||||
@@ -1066,6 +1066,14 @@ class LanguageServerRegistry:
|
||||
return self._registered_language_servers[key]
|
||||
raise ValueError(f"Unknown language server key: '{key}'; Valid keys: {self.get_keys()}")
|
||||
|
||||
def iter_registered_ls_ids(self) -> Iterator[LanguageServerIdLike]:
|
||||
"""
|
||||
Iterate over all registered language servers (built-in + externally-registered via
|
||||
entry points). Order follows ``get_keys()`` (alphabetical).
|
||||
"""
|
||||
for key in self.get_keys():
|
||||
yield self._registered_language_servers[key]
|
||||
|
||||
def register(self, ls_id: LanguageServerIdLike, allow_override: bool = False) -> None:
|
||||
"""
|
||||
:param ls_id: the identifier to register
|
||||
|
||||
Reference in new issue
Block a user