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:
riverolfe authored and GitHub committed 2026-09-22 12:43:51 +02:00
1 parent 9f19a79a04
commit dc97aba74a
4 files changed
+35 -22

No files matched your search

+6 -5
View File
@@ -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,
+16 -12
View File
@@ -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()
+4 -4
View File
@@ -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
+9 -1
View File
@@ -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