Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/serena/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/serena/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down
3 changes: 3 additions & 0 deletions src/serena/config/serena_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion src/serena/project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path
from typing import Any

import pathspec

Expand Down Expand Up @@ -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,
Copy link
Contributor

@opcode81 opcode81 Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we just add SerenaConfig as an argument? What do you think?
(I am not sure what I prefer)

) -> SolidLanguageServer:
"""
Create a language server for a project. Note that you will have to start it
Expand All @@ -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(
Expand All @@ -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),
)
1 change: 0 additions & 1 deletion src/serena/resources/project.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/serena/resources/serena_config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
Copy link
Contributor

@opcode81 opcode81 Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you like the name "ls_specifics"? I think it's a bit vague.
How about "ls_specific_settings" or "ls_advanced_settings"?

# 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

Expand Down
2 changes: 1 addition & 1 deletion src/serena/util/file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 15 additions & 6 deletions src/solidlsp/language_servers/csharp_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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}"
Expand Down
22 changes: 17 additions & 5 deletions src/solidlsp/language_servers/intelephense.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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):
Expand Down
136 changes: 11 additions & 125 deletions src/solidlsp/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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

Expand Down
Loading
Loading