Allow passing LS implementation specific options. (#524)

* Allow passing LS implementation specific options. Made use of it in csharp and intelephense LS
* Bidirectional mapping from Language to the corresponding LS implementation Use this as key for ls_specifics.

Fix to swift installation in CI authored-by: maksim.r <avalab>
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-09-13 00:36:21 +02:00
1 parent 75bdee7d54
commit 5d33a301f3
13 files changed
+198 -158

No files matched your search

+3 -1
View File
@@ -106,6 +106,9 @@ jobs:
if: runner.os == 'Linux'
run: |
echo "=== Installing swiftly on Ubuntu ==="
# Install dependencies BEFORE Swift to avoid exit code 1
sudo apt-get update
sudo apt-get -y install libcurl4-openssl-dev
curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \
tar zxf swiftly-$(uname -m).tar.gz && \
./swiftly init --quiet-shell-followup && \
@@ -113,7 +116,6 @@ jobs:
hash -r
swiftly install --use 6.1.2
swiftly use 6.1.2
sudo apt-get -y install libcurl4-openssl-dev
echo "=== Adding Swift toolchain to PATH ==="
echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH
echo "Swiftly installed successfully!"
+1
View File
@@ -589,6 +589,7 @@ class SerenaAgent:
log_level=self.serena_config.log_level,
ls_timeout=ls_timeout,
trace_lsp_communication=self.serena_config.trace_lsp_communication,
ls_specific_settings=self.serena_config.ls_specific_settings,
)
log.info(f"Starting the language server for {self._active_project.project_name}")
self.language_server.start()
+2 -1
View File
@@ -451,9 +451,10 @@ class ProjectCommands(AutoRegisteringGroup):
def _index_project(project: str, log_level: str, timeout: float) -> None:
lvl = logging.getLevelNamesMapping()[log_level.upper()]
logging.configure(level=lvl)
serena_config = SerenaConfig.from_config_file()
proj = Project.load(os.path.abspath(project))
click.echo(f"Indexing symbols in project {project}…")
ls = proj.create_language_server(log_level=lvl, ls_timeout=timeout)
ls = proj.create_language_server(log_level=lvl, ls_timeout=timeout, ls_specific_settings=serena_config.ls_specific_settings)
log_file = os.path.join(project, ".serena", "logs", "indexing.txt")
collected_exceptions: list[Exception] = []
+3
View File
@@ -355,6 +355,8 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
through the global configuration.
"""
ls_specific_settings: dict = field(default_factory=dict)
"""Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""
CONFIG_FILE = "serena_config.yml"
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
@@ -459,6 +461,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
)
instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000)
instance.ls_specific_settings = loaded_commented_yaml.get("ls_specific_settings", {})
# re-save the configuration file if any migrations were performed
if num_project_migrations > 0:
+9 -1
View File
@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path
from typing import Any
import pathspec
@@ -277,6 +278,7 @@ class Project:
log_level: int = logging.INFO,
ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, Any] | None = None,
) -> SolidLanguageServer:
"""
Create a language server for a project. Note that you will have to start it
@@ -287,6 +289,8 @@ class Project:
:param log_level: the log level for the language server
:param ls_timeout: the timeout for the language server
:param trace_lsp_communication: whether to trace LSP communication
:param ls_specific_settings: optional LS specific configuration of the language server,
see docstrings in the inits of subclasses of SolidLanguageServer to see what values may be passed.
:return: the language server
"""
ls_config = LanguageServerConfig(
@@ -302,5 +306,9 @@ class Project:
ls_logger,
self.project_root,
timeout=ls_timeout,
solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME, project_data_relative_path=SERENA_MANAGED_DIR_NAME),
solidlsp_settings=SolidLSPSettings(
solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME,
project_data_relative_path=SERENA_MANAGED_DIR_NAME,
ls_specific_settings=ls_specific_settings,
),
)
@@ -19,7 +19,6 @@ ignored_paths: []
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
@@ -33,6 +33,13 @@ trace_lsp_communication: False
# whether to trace the communication between Serena and the language servers.
# This is useful for debugging language server issues.
ls_specific_settings: {}
# Added on 23.08.2025
# Advanced configuration option allowing to configure language server implementation specific options. Maps the language
# (same entry as in project.yml) to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp to see which options are available.
# No documentation on options means no options are available
tool_timeout: 240
# timeout, in seconds, after which tool executions are terminated
+1 -1
View File
@@ -154,7 +154,7 @@ class GitignoreParser:
"""
queue: list[str] = [self.repo_root]
def scan(abs_path: str | None):
def scan(abs_path: str | None) -> Iterator[str]:
for entry in os.scandir(abs_path):
if entry.is_dir(follow_symlinks=follow_symlinks):
queue.append(entry.path)
@@ -174,6 +174,9 @@ class CSharpLanguageServer(SolidLanguageServer):
"""
Provides C# specific instantiation of the LanguageServer class using Microsoft.CodeAnalysis.LanguageServer.
This is the official Roslyn-based language server from Microsoft.
You can pass the following entries in ls_specific_settings["csharp"]:
- dotnet_runtime_url: will override the URL from RUNTIME_DEPENDENCIES
"""
def __init__(
@@ -269,7 +272,7 @@ class CSharpLanguageServer(SolidLanguageServer):
@classmethod
def _ensure_dotnet_runtime(
cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# Check if dotnet is already available on the system
@@ -285,7 +288,7 @@ class CSharpLanguageServer(SolidLanguageServer):
pass
# Download .NET 9 runtime using config
return cls._ensure_dotnet_runtime_from_config(logger, runtime_dep, solidlsp_settings)
return cls._ensure_dotnet_runtime_from_config(logger, dotnet_runtime_dep, solidlsp_settings)
@classmethod
def _ensure_language_server(
@@ -404,7 +407,7 @@ class CSharpLanguageServer(SolidLanguageServer):
@classmethod
def _ensure_dotnet_runtime_from_config(
cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Ensure .NET 9 runtime is available using runtime dependency configuration.
@@ -424,7 +427,7 @@ class CSharpLanguageServer(SolidLanguageServer):
# Download .NET 9 runtime using config
dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0"
dotnet_exe = dotnet_dir / runtime_dep.binary_name
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
if dotnet_exe.exists():
logger.log(f"Using cached .NET runtime from {dotnet_exe}", logging.INFO)
@@ -434,8 +437,16 @@ class CSharpLanguageServer(SolidLanguageServer):
logger.log("Downloading .NET 9 runtime...", logging.INFO)
dotnet_dir.mkdir(parents=True, exist_ok=True)
url = runtime_dep.url
archive_type = runtime_dep.archive_type
custom_dotnet_runtime_url = solidlsp_settings.ls_specific_settings.get(cls.get_language_enum_instance(), {}).get(
"dotnet_runtime_url"
)
if custom_dotnet_runtime_url is not None:
logger.log(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}", logging.INFO)
url = custom_dotnet_runtime_url
else:
url = dotnet_runtime_dep.url
archive_type = dotnet_runtime_dep.archive_type
# Download the runtime
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
+17 -5
View File
@@ -24,6 +24,10 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
class Intelephense(SolidLanguageServer):
"""
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
You can pass the following entries in ls_specific_settings["php"]:
- maxMemory
- maxFileSize
"""
@override
@@ -98,10 +102,9 @@ class Intelephense(SolidLanguageServer):
)
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Intelephense Language Server.
Returns the initialization params for the Intelephense Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
@@ -123,12 +126,21 @@ class Intelephense(SolidLanguageServer):
}
],
}
initialization_options = {}
# Add license key if provided via environment variable
license_key = os.environ.get("INTELEPHENSE_LICENSE_KEY")
if license_key:
initialize_params["initializationOptions"] = {"licenceKey": license_key}
initialization_options["licenceKey"] = license_key
custom_intelephense_settings = self._solidlsp_settings.ls_specific_settings.get(self.get_language_enum_instance(), {})
max_memory = custom_intelephense_settings.get("maxMemory")
max_file_size = custom_intelephense_settings.get("maxFileSize")
if max_memory is not None:
initialization_options["intelephense.maxMemory"] = max_memory
if max_file_size is not None:
initialization_options["intelephense.files.maxSize"] = max_file_size
initialize_params["initializationOptions"] = initialization_options
return initialize_params
def _start_server(self):
+11 -140
View File
@@ -94,6 +94,10 @@ class SolidLanguageServer(ABC):
"""
return dirname.startswith(".")
@classmethod
def get_language_enum_instance(cls) -> Language:
return Language.from_ls_class(cls)
@classmethod
def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
"""
@@ -131,154 +135,21 @@ class SolidLanguageServer(ABC):
If language is JS/TS, then ensure that node (v18.16.0 or higher) is installed and in PATH.
:param repository_root_path: The root path of the repository.
:param config: The Multilspy configuration.
:param config: language server configuration.
:param logger: The logger to use.
:param timeout: the timeout for requests to the language server. If None, no timeout will be used.
:param solidlsp_settings: additional settings
:return LanguageServer: A language specific LanguageServer instance.
"""
ls: SolidLanguageServer
if solidlsp_settings is None:
solidlsp_settings = SolidLSPSettings()
if config.code_language == Language.PYTHON:
from solidlsp.language_servers.pyright_server import (
PyrightServer,
)
ls = PyrightServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.PYTHON_JEDI:
from solidlsp.language_servers.jedi_server import JediServer
ls = JediServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.JAVA:
from solidlsp.language_servers.eclipse_jdtls import (
EclipseJDTLS,
)
ls = EclipseJDTLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.KOTLIN:
from solidlsp.language_servers.kotlin_language_server import (
KotlinLanguageServer,
)
ls = KotlinLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.RUST:
from solidlsp.language_servers.rust_analyzer import (
RustAnalyzer,
)
ls = RustAnalyzer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CSHARP:
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
ls = CSharpLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CSHARP_OMNISHARP:
from solidlsp.language_servers.omnisharp import OmniSharp
ls = OmniSharp(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TYPESCRIPT:
from solidlsp.language_servers.typescript_language_server import (
TypeScriptLanguageServer,
)
ls = TypeScriptLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TYPESCRIPT_VTS:
# VTS based Language Server implementation, need to experiment to see if it improves performance
from solidlsp.language_servers.vts_language_server import VtsLanguageServer
ls = VtsLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.GO:
from solidlsp.language_servers.gopls import Gopls
ls = Gopls(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.RUBY:
from solidlsp.language_servers.ruby_lsp import RubyLsp
ls = RubyLsp(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.RUBY_SOLARGRAPH:
from solidlsp.language_servers.solargraph import Solargraph
ls = Solargraph(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.DART:
from solidlsp.language_servers.dart_language_server import DartLanguageServer
ls = DartLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CPP:
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
ls = ClangdLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.PHP:
from solidlsp.language_servers.intelephense import Intelephense
ls = Intelephense(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.R:
from solidlsp.language_servers.r_language_server import RLanguageServer
ls = RLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CLOJURE:
from solidlsp.language_servers.clojure_lsp import ClojureLSP
ls = ClojureLSP(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.ELIXIR:
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
ls = ElixirTools(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TERRAFORM:
from solidlsp.language_servers.terraform_ls import TerraformLS
ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.SWIFT:
from solidlsp.language_servers.sourcekit_lsp import SourceKitLSP
ls = SourceKitLSP(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.BASH:
from solidlsp.language_servers.bash_language_server import BashLanguageServer
ls = BashLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.ZIG:
from solidlsp.language_servers.zls import ZigLanguageServer
ls = ZigLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.NIX:
from solidlsp.language_servers.nixd_ls import NixLanguageServer
ls = NixLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.LUA:
from solidlsp.language_servers.lua_ls import LuaLanguageServer
ls = LuaLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.ERLANG:
from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer
ls = ErlangLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.AL:
from solidlsp.language_servers.al_language_server import ALLanguageServer
ls = ALLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise SolidLSPException(f"Language {config.code_language} is not supported")
ls_class = config.code_language.get_ls_class()
# For now, we assume that all language server implementations have the same signature of the constructor
# (which, unfortunately, differs from the signature of the base class).
# If this assumption is ever violated, we need branching logic here.
ls = ls_class(config, logger, repository_root_path, solidlsp_settings) # type: ignore
ls.set_request_timeout(timeout)
return ls
+123 -1
View File
@@ -6,7 +6,10 @@ import fnmatch
from collections.abc import Iterable
from dataclasses import dataclass, field
from enum import Enum
from typing import Self
from typing import TYPE_CHECKING, Self
if TYPE_CHECKING:
from solidlsp import SolidLanguageServer
class FilenameMatcher:
@@ -136,6 +139,125 @@ class Language(str, Enum):
case _:
raise ValueError(f"Unhandled language: {self}")
def get_ls_class(self) -> type["SolidLanguageServer"]:
match self:
case self.PYTHON:
from solidlsp.language_servers.pyright_server import PyrightServer
return PyrightServer
case self.PYTHON_JEDI:
from solidlsp.language_servers.jedi_server import JediServer
return JediServer
case self.JAVA:
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
return EclipseJDTLS
case self.KOTLIN:
from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
return KotlinLanguageServer
case self.RUST:
from solidlsp.language_servers.rust_analyzer import RustAnalyzer
return RustAnalyzer
case self.CSHARP:
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
return CSharpLanguageServer
case self.CSHARP_OMNISHARP:
from solidlsp.language_servers.omnisharp import OmniSharp
return OmniSharp
case self.TYPESCRIPT:
from solidlsp.language_servers.typescript_language_server import TypeScriptLanguageServer
return TypeScriptLanguageServer
case self.TYPESCRIPT_VTS:
from solidlsp.language_servers.vts_language_server import VtsLanguageServer
return VtsLanguageServer
case self.GO:
from solidlsp.language_servers.gopls import Gopls
return Gopls
case self.RUBY:
from solidlsp.language_servers.solargraph import Solargraph
return Solargraph
case self.DART:
from solidlsp.language_servers.dart_language_server import DartLanguageServer
return DartLanguageServer
case self.CPP:
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
return ClangdLanguageServer
case self.PHP:
from solidlsp.language_servers.intelephense import Intelephense
return Intelephense
case self.CLOJURE:
from solidlsp.language_servers.clojure_lsp import ClojureLSP
return ClojureLSP
case self.ELIXIR:
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
return ElixirTools
case self.TERRAFORM:
from solidlsp.language_servers.terraform_ls import TerraformLS
return TerraformLS
case self.SWIFT:
from solidlsp.language_servers.sourcekit_lsp import SourceKitLSP
return SourceKitLSP
case self.BASH:
from solidlsp.language_servers.bash_language_server import BashLanguageServer
return BashLanguageServer
case self.ZIG:
from solidlsp.language_servers.zls import ZigLanguageServer
return ZigLanguageServer
case self.NIX:
from solidlsp.language_servers.nixd_ls import NixLanguageServer
return NixLanguageServer
case self.LUA:
from solidlsp.language_servers.lua_ls import LuaLanguageServer
return LuaLanguageServer
case self.ERLANG:
from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer
return ErlangLanguageServer
case self.AL:
from solidlsp.language_servers.al_language_server import ALLanguageServer
return ALLanguageServer
case self.R:
from solidlsp.language_servers.r_language_server import RLanguageServer
return RLanguageServer
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
"""
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}")
@dataclass
class LanguageServerConfig:
+4 -1
View File
@@ -4,7 +4,8 @@ Defines settings for Solid-LSP
import os
import pathlib
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any
@dataclass
@@ -19,6 +20,8 @@ class SolidLSPSettings:
For instance, if this is ".solidlsp" and the project is located at "/home/user/myproject",
then Solid-LSP will store project-specific data in "/home/user/myproject/.solidlsp".
"""
ls_specific_settings: dict[str, Any] = field(default_factory=dict)
"""Mapping from language server class names to any specifics that the language server may make use of."""
def __post_init__(self):
os.makedirs(str(self.solidlsp_dir), exist_ok=True)