diff --git a/src/serena/agent.py b/src/serena/agent.py index 57f95410..08507b28 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -585,6 +585,7 @@ def reset_language_server(self) -> None: log_level=self.serena_config.log_level, ls_timeout=ls_timeout, trace_lsp_communication=self.serena_config.trace_lsp_communication, + ls_specifics=self.serena_config.ls_specifics, ) log.info(f"Starting the language server for {self._active_project.project_name}") self.language_server.start() diff --git a/src/serena/cli.py b/src/serena/cli.py index d00e0fc0..fc93ab93 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -451,9 +451,10 @@ def index_deprecated(project: str, log_level: str, timeout: float) -> None: 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_specifics=serena_config.ls_specifics) log_file = os.path.join(project, ".serena", "logs", "indexing.txt") collected_exceptions: list[Exception] = [] diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 2eac7512..7595072e 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -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_specifics: 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 @@ def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": "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_specifics = loaded_commented_yaml.get("ls_specifics", {}) # re-save the configuration file if any migrations were performed if num_project_migrations > 0: diff --git a/src/serena/project.py b/src/serena/project.py index a58e037c..418d41e4 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -1,6 +1,7 @@ import logging import os from pathlib import Path +from typing import Any import pathspec @@ -266,6 +267,7 @@ def create_language_server( log_level: int = logging.INFO, ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5, trace_lsp_communication: bool = False, + ls_specifics: dict[Language, Any] | None = None, ) -> SolidLanguageServer: """ Create a language server for a project. Note that you will have to start it @@ -276,6 +278,8 @@ def create_language_server( :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_specifics: 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( @@ -291,5 +295,5 @@ def create_language_server( ls_logger, self.project_root, timeout=ls_timeout, - solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME), + solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME, ls_specifics=ls_specifics), ) diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index a51cfcf5..59ab269c 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -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, diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 9d7bce7c..40250935 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -33,6 +33,12 @@ trace_lsp_communication: False # whether to trace the communication between Serena and the language servers. # This is useful for debugging language server issues. +ls_specifics: {} +# Added on 23.08.2025 +# Advanced configuration option allowing to configure language server implementation specific options. Maps the language server class name 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 diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 6f9739af..15ba3ba3 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -154,7 +154,7 @@ def _iter_gitignore_files(self, follow_symlinks: bool = False) -> Iterator[str]: """ 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) diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index a0d5408d..7d38f834 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -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_specifics["csharp"]: + - dotnet_runtime_url: will override the URL from RUNTIME_DEPENDENCIES """ def __init__( @@ -269,7 +272,7 @@ def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, Runti @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 @@ def _ensure_dotnet_runtime( 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 @@ def _download_nuget_package_direct( @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 @@ def _ensure_dotnet_runtime_from_config( # 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,14 @@ def _ensure_dotnet_runtime_from_config( 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_specifics.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}" diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index ee3a61b9..05003067 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -24,6 +24,10 @@ class Intelephense(SolidLanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. + + You can pass the following entries in ls_specifics["php"]: + - maxMemory + - maxFileSize """ @override @@ -98,10 +102,9 @@ def __init__( ) 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 @@ def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: } ], } - + 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_specifics.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): diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 8d8b1f2e..adc69946 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -92,6 +92,10 @@ def is_ignored_dirname(self, dirname: str) -> bool: """ 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: """ @@ -129,139 +133,21 @@ def create( 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.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.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) - - 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 diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 946df96a..efd5e6d1 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -6,7 +6,10 @@ 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: @@ -124,6 +127,117 @@ def get_source_fn_matcher(self) -> FilenameMatcher: 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 _: + 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: diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 0b956bff..36abfde1 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -4,12 +4,15 @@ import os import pathlib -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Any @dataclass class SolidLSPSettings: solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp") + ls_specifics: 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)