From fc71d712850ffb7e5d46ef99f797e75766e3dd44 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Mon, 21 Jul 2025 23:35:27 +0200 Subject: [PATCH 01/49] LS: use home directory for language server installations Added a migration logic that will omit unnecessary installation Used the correct LS installation path for clojure --- src/solidlsp/language_servers/clojure_lsp.py | 7 +++---- src/solidlsp/ls.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index cae8abc2..5939a7e4 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -95,17 +95,16 @@ class ClojureLSP(SolidLanguageServer): self.resolve_main_method_available = threading.Event() self.service_ready_event = threading.Event() - @staticmethod - def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" verify_clojure_cli() deps = ClojureLSP.runtime_dependencies dependency = deps.single_for_current_platform() - clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp") + clojurelsp_ls_dir = cls.ls_resources_dir() clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir) if not os.path.exists(clojurelsp_executable_path): - os.makedirs(clojurelsp_ls_dir, exist_ok=True) logger.log( f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}", logging.INFO, diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index d68331b0..6c67f574 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -5,6 +5,7 @@ import logging import os import pathlib import pickle +import shutil import subprocess import threading from abc import ABC, abstractmethod @@ -94,7 +95,19 @@ class SolidLanguageServer(ABC): Returns the directory where the language server resources are downloaded. This is used to store language server binaries, configuration files, etc. """ - result = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__) + from serena.constants import SERENA_MANAGED_DIR_IN_HOME + + result = os.path.join(SERENA_MANAGED_DIR_IN_HOME, "language_servers", "static", cls.__name__) + + # Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home + pre_migration_ls_resources_dir = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__) + if os.path.exists(pre_migration_ls_resources_dir): + if os.path.exists(result): + # if the directory already exists, we just remove the old resources + shutil.rmtree(result, ignore_errors=True) + else: + # move old resources to the new location + shutil.move(pre_migration_ls_resources_dir, result) if mkdir: os.makedirs(result, exist_ok=True) return result From b63acf2dbe021148c5525a54212b482fdf488726 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 24 Jul 2025 23:51:28 +0200 Subject: [PATCH 02/49] GH: adjusted dir for language servers cache --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index cf3b93a4..91dcd610 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -77,7 +77,7 @@ jobs: id: cache-language-servers uses: actions/cache@v3 with: - path: src/solidlsp/language_servers/static + path: ~/.serena/language_servers/static key: language-servers-${{ runner.os }}-v1 restore-keys: | language-servers-${{ runner.os }}- From debcccbee2ad45fb149ac4073520d0102cc6f0f3 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 24 Jul 2025 23:55:29 +0200 Subject: [PATCH 03/49] Elixir: refactored to be in line with the other LS implementations --- .../elixir_tools/elixir_tools.py | 178 +++++++++++------- .../elixir_tools/initialize_params.json | 97 ---------- .../elixir_tools/runtime_dependencies.json | 33 ---- 3 files changed, 111 insertions(+), 197 deletions(-) delete mode 100644 src/solidlsp/language_servers/elixir_tools/initialize_params.json delete mode 100644 src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 96674722..16df9027 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -1,4 +1,3 @@ -import json import logging import os import pathlib @@ -6,17 +5,18 @@ import stat import subprocess import threading import time -from pathlib import PurePath from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger -from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from ..common import RuntimeDependency + class ElixirTools(SolidLanguageServer): """ @@ -68,8 +68,8 @@ class ElixirTools(SolidLanguageServer): return filtered_response - @staticmethod - def _get_elixir_version(): + @classmethod + def _get_elixir_version(cls): """Get the installed Elixir version or None if not found.""" try: result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False) @@ -79,13 +79,14 @@ class ElixirTools(SolidLanguageServer): return None return None - def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for Next LS. Downloads the Next LS binary for the current platform and returns the path to the executable. """ # Check if Elixir is available first - elixir_version = self._get_elixir_version() + elixir_version = cls._get_elixir_version() if not elixir_version: raise RuntimeError( "Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH." @@ -93,62 +94,68 @@ class ElixirTools(SolidLanguageServer): logger.log(f"Found Elixir: {elixir_version}", logging.INFO) - platformId = PlatformUtils.get_platform_id() - - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f: - runtimeDependencies = json.load(f) - del runtimeDependencies["_description"] - - os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True) - - # Map platform IDs to runtime dependency keys - platform_mapping = { - "linux-x64": "linux-x64", - "osx-x64": "darwin-x64", - "osx-arm64": "darwin-arm64", - "darwin-x64": "darwin-x64", - "darwin-arm64": "darwin-arm64", - "win-x64": "win-x64", - } - - platform_key = platform_mapping.get(platformId.value) - if not platform_key: - raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}") + platform_id = PlatformUtils.get_platform_id() # Check for Windows and provide a helpful error message - if platformId.value.startswith("win"): + if platform_id.value.startswith("win"): raise RuntimeError( "Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. " "Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS." ) - dependency = runtimeDependencies["next_ls"][platform_key] - next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) - os.makedirs(next_ls_dir, exist_ok=True) + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + ] + assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment" - executable_path = str(PurePath(next_ls_dir, dependency["executable_name"])) - binary_path = str(PurePath(next_ls_dir, dependency["binary_name"])) + next_ls_dir = os.path.join(cls.ls_resources_dir(), "next-ls") + + # Define runtime dependencies inline + runtime_deps = { + PlatformId.LINUX_x64: RuntimeDependency( + id="next_ls_linux_amd64", + platform_id="linux-x64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64", + archive_type="binary", + binary_name="next_ls_linux_amd64", + extract_path="next_ls", + ), + PlatformId.OSX_x64: RuntimeDependency( + id="next_ls_darwin_amd64", + platform_id="osx-x64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64", + archive_type="binary", + binary_name="next_ls_darwin_amd64", + extract_path="next_ls", + ), + PlatformId.OSX_arm64: RuntimeDependency( + id="next_ls_darwin_arm64", + platform_id="osx-arm64", + url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64", + archive_type="binary", + binary_name="next_ls_darwin_arm64", + extract_path="next_ls", + ), + } + + dependency = runtime_deps[platform_id] + executable_path = os.path.join(next_ls_dir, "nextls") + binary_path = os.path.join(next_ls_dir, dependency.binary_name) if not os.path.exists(executable_path): - logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO) - FileUtils.download_file(logger, dependency["url"], binary_path) + logger.log(f"Downloading Next LS binary from {dependency.url}", logging.INFO) + FileUtils.download_file(logger, dependency.url, binary_path) # Make the binary executable on Unix-like systems - if not platformId.value.startswith("win"): - os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - # Create a symlink or copy with the expected name + # Create a symlink with the expected name if binary_path != executable_path: if os.path.exists(executable_path): os.remove(executable_path) - if platformId.value.startswith("win"): - # On Windows, copy the file - import shutil - - shutil.copy2(binary_path, executable_path) - else: - # On Unix-like systems, create a symlink - os.symlink(os.path.basename(binary_path), executable_path) + os.symlink(os.path.basename(binary_path), executable_path) assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}" @@ -156,7 +163,7 @@ class ElixirTools(SolidLanguageServer): return executable_path def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): - nextls_executable_path = self.setupRuntimeDependencies(logger, config) + nextls_executable_path = self._setup_runtime_dependencies(logger, config) super().__init__( config, @@ -171,29 +178,66 @@ class ElixirTools(SolidLanguageServer): # Set generous timeout for Next LS which can be slow to initialize and respond self.set_request_timeout(180.0) # 60 seconds for all environments - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Next LS Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: - d = json.load(f) + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "processId": os.getpid(), + "locale": "en", + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "initializationOptions": { + "mix_env": "dev", + "mix_target": "host", + "experimental": {"completions": {"enable": False}}, + "extensions": {"credo": {"enable": True, "cli_options": []}}, + }, + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": { + "dynamicRegistration": True, + "completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]}, + }, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "formatting": {"dynamicRegistration": True}, + "codeAction": { + "dynamicRegistration": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + }, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "executeCommand": {"dynamicRegistration": True}, + }, + }, + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + } - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d + return initialize_params def _start_server(self): """Start Next LS server process""" diff --git a/src/solidlsp/language_servers/elixir_tools/initialize_params.json b/src/solidlsp/language_servers/elixir_tools/initialize_params.json deleted file mode 100644 index 51ce266f..00000000 --- a/src/solidlsp/language_servers/elixir_tools/initialize_params.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "initializationOptions": { - "mix_env": "dev", - "mix_target": "host", - "experimental": { - "completions": { - "enable": false - } - }, - "extensions": { - "credo": { - "enable": true, - "cli_options": [] - } - } - }, - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ] - } - }, - "definition": { - "dynamicRegistration": true - }, - "references": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "hierarchicalDocumentSymbolSupport": true, - "symbolKind": { - "valueSet": [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "formatting": { - "dynamicRegistration": true - }, - "codeAction": { - "dynamicRegistration": true, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - } - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "executeCommand": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json b/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json deleted file mode 100644 index 32641a0d..00000000 --- a/src/solidlsp/language_servers/elixir_tools/runtime_dependencies.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "_description": "This file lists the runtime dependencies for the Elixir Language Server (Next LS)", - "next_ls": { - "linux-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_linux_amd64", - "executable_name": "nextls" - }, - "darwin-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_darwin_amd64", - "executable_name": "nextls" - }, - "darwin-arm64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_darwin_arm64", - "executable_name": "nextls" - }, - "win-x64": { - "url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_windows_amd64.exe", - "archiveType": "binary", - "relative_extraction_path": "next_ls", - "binary_name": "next_ls_windows_amd64.exe", - "executable_name": "nextls.exe" - } - } -} \ No newline at end of file From 435e745fb55071d2d6803a4f2d00d346acfc1e2a Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Fri, 25 Jul 2025 00:09:41 +0200 Subject: [PATCH 04/49] Added makedirs in download_file --- src/solidlsp/ls_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index b46491ec..5110fe86 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -183,6 +183,7 @@ class FileUtils: """ Downloads the file from the given URL to the given {target_path} """ + os.makedirs(os.path.dirname(target_path), exist_ok=True) try: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: From aeb871c0405876a1683e8aed3ed1b7651d31e06a Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Fri, 25 Jul 2025 14:35:10 +0200 Subject: [PATCH 05/49] Refactoring: use non-static SolidLSPSettings, WIP (functionality unfinished) --- .../language_servers/eclipse_jdtls.py | 16 +++--- src/solidlsp/ls.py | 54 +++++++++++-------- src/solidlsp/settings.py | 24 ++++----- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 5ebb378c..3ac80046 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -44,7 +44,9 @@ class EclipseJDTLS(SolidLanguageServer): The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -55,7 +57,7 @@ class EclipseJDTLS(SolidLanguageServer): # ws_dir is the workspace directory for the EclipseJDTLS server ws_dir = str( PurePath( - SolidLSPSettings.get_language_server_directory(), + solidlsp_settings.ls_resources_dir, "EclipseJDTLS", "workspaces", uuid.uuid4().hex, @@ -63,15 +65,15 @@ class EclipseJDTLS(SolidLanguageServer): ) # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) + shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex")) + os.makedirs(shared_cache_location, exist_ok=True) + os.makedirs(ws_dir, exist_ok=True) jre_path = self.runtime_dependency_paths.jre_path lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path - os.makedirs(ws_dir, exist_ok=True) - data_dir = str(PurePath(ws_dir, "data_dir")) jdtls_config_path = str(PurePath(ws_dir, "config_path")) @@ -132,7 +134,9 @@ class EclipseJDTLS(SolidLanguageServer): self.intellicode_enable_command_available = threading.Event() self.initialize_searcher_command_available = threading.Event() - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings + ) @override def is_ignored_dirname(self, dirname: str) -> bool: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6c67f574..e47e83ca 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -35,6 +35,7 @@ from solidlsp.lsp_protocol_handler.server import ( ProcessLaunchInfo, StringDict, ) +from solidlsp.settings import SolidLSPSettings GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation] @@ -90,14 +91,14 @@ class SolidLanguageServer(ABC): return dirname.startswith(".") @classmethod - def ls_resources_dir(cls, mkdir: bool = True) -> str: + def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str: """ Returns the directory where the language server resources are downloaded. This is used to store language server binaries, configuration files, etc. """ - from serena.constants import SERENA_MANAGED_DIR_IN_HOME - - result = os.path.join(SERENA_MANAGED_DIR_IN_HOME, "language_servers", "static", cls.__name__) + if solidlsp_settings is None: + solidlsp_settings = SolidLSPSettings() + result = os.path.join(solidlsp_settings.ls_resources_dir, cls.__name__) # Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home pre_migration_ls_resources_dir = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__) @@ -114,7 +115,12 @@ class SolidLanguageServer(ABC): @classmethod def create( - cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None + cls, + config: LanguageServerConfig, + logger: LanguageServerLogger, + repository_root_path: str, + solidlsp_settings: SolidLSPSettings, + timeout: float | None = None, ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -135,90 +141,90 @@ class SolidLanguageServer(ABC): PyrightServer, ) - ls = PyrightServer(config, logger, repository_root_path) + 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) + 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) + 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) + 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) + 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) + ls = CSharpLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) elif config.code_language == Language.CSHARP_OMNISHARP: - from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp + from solidlsp.language_servers.omnisharp import OmniSharp - ls = OmniSharp(config, logger, repository_root_path) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings) else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) @@ -234,6 +240,7 @@ class SolidLanguageServer(ABC): repository_root_path: str, process_launch_info: ProcessLaunchInfo, language_id: str, + solidlsp_settings: SolidLSPSettings, ): """ Initializes a LanguageServer instance. @@ -248,6 +255,7 @@ class SolidLanguageServer(ABC): The command must pass appropriate flags to the binary, so that it runs in the stdio mode, as opposed to HTTP, TCP modes supported by some language servers. """ + self._solidlsp_settings = solidlsp_settings self.logger = logger self.repository_root_path: str = repository_root_path self.logger.log( diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 38b30102..0b956bff 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -4,21 +4,17 @@ Defines settings for Solid-LSP import os import pathlib +from dataclasses import dataclass +@dataclass class SolidLSPSettings: - @staticmethod - def get_language_server_directory() -> str: - """Returns the directory for language servers""" - user_home = pathlib.Path.home() - multilspy_dir = str(pathlib.PurePath(user_home, ".multilspy")) - lsp_dir = str(pathlib.PurePath(multilspy_dir, "lsp")) - os.makedirs(lsp_dir, exist_ok=True) - return lsp_dir + solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp") - @staticmethod - def get_global_cache_directory() -> str: - """Returns the cache directory""" - global_cache_dir = os.path.join(str(pathlib.Path.home()), ".multilspy", "global_cache") - os.makedirs(global_cache_dir, exist_ok=True) - return global_cache_dir + def __post_init__(self): + os.makedirs(str(self.solidlsp_dir), exist_ok=True) + os.makedirs(str(self.ls_resources_dir), exist_ok=True) + + @property + def ls_resources_dir(self): + return os.path.join(str(self.solidlsp_dir), "language_servers", "static") From 65259254fddbb20ac28ec64d1b5f182d8a32645d Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Fri, 25 Jul 2025 15:17:03 +0200 Subject: [PATCH 06/49] CC Vanilla MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tokens: 5,213,813 Prompts: - I recently added the non-optional parameter solidlsp_settings to the init of SolidLanguageServer, the create method, and the ls_resources_dir method. All language server subclasses now need to be adjusted │ │ accordingly to use solidlsp_settings. Do that. You can have a look at relevant memories in .serena/memories to understand the repo structure - don't execute elixir tests, they hang --- src/serena/project.py | 3 ++ .../clangd_language_server.py | 16 +++++-- src/solidlsp/language_servers/clojure_lsp.py | 14 ++++-- .../csharp_language_server.py | 48 ++++++++++++------- .../language_servers/dart_language_server.py | 10 ++-- .../language_servers/eclipse_jdtls.py | 12 +++-- .../elixir_tools/elixir_tools.py | 14 ++++-- src/solidlsp/language_servers/gopls.py | 6 ++- src/solidlsp/language_servers/intelephense.py | 22 +++++++-- src/solidlsp/language_servers/jedi_server.py | 6 ++- .../kotlin_language_server.py | 14 ++++-- src/solidlsp/language_servers/omnisharp.py | 19 +++++--- .../language_servers/pyright_server.py | 6 ++- .../language_servers/rust_analyzer.py | 14 ++++-- src/solidlsp/language_servers/solargraph.py | 6 ++- src/solidlsp/language_servers/terraform_ls.py | 14 ++++-- .../typescript_language_server.py | 14 ++++-- .../language_servers/vts_language_server.py | 14 ++++-- src/solidlsp/ls.py | 4 +- test/conftest.py | 6 ++- test/solidlsp/csharp/test_csharp_basic.py | 9 +++- 21 files changed, 192 insertions(+), 79 deletions(-) diff --git a/src/serena/project.py b/src/serena/project.py index b73a252d..d6a7ba79 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -5,11 +5,13 @@ from pathlib import Path import pathspec from serena.config.serena_config import DEFAULT_TOOL_TIMEOUT, ProjectConfig +from serena.constants import SERENA_MANAGED_DIR_IN_HOME from serena.text_utils import MatchedConsecutiveLines, search_files from serena.util.file_system import GitignoreParser, match_path from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -271,4 +273,5 @@ class Project: ls_logger, self.project_root, timeout=ls_timeout, + solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME), ) diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index b8845d84..a770d78e 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -13,6 +13,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -24,25 +25,30 @@ class ClangdLanguageServer(SolidLanguageServer): Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clangd_executable_path = self._setup_runtime_dependencies(logger, config) + clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() - @staticmethod - def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + @classmethod + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for ClangdLanguageServer and return the command to start the server. """ @@ -75,7 +81,7 @@ class ClangdLanguageServer(SolidLanguageServer): ] ) - clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") + clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd") dep = deps.single_for_current_platform() clangd_executable_path = deps.binary_path(clangd_ls_dir) if not os.path.exists(clangd_executable_path): diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index 5939a7e4..f973db15 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -78,17 +79,20 @@ class ClojureLSP(SolidLanguageServer): ] ) - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config) + clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path), "clojure", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @@ -96,13 +100,15 @@ class ClojureLSP(SolidLanguageServer): self.service_ready_event = threading.Event() @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """Setup runtime dependencies for clojure-lsp and return the command to start the server.""" verify_clojure_cli() deps = ClojureLSP.runtime_dependencies dependency = deps.single_for_current_platform() - clojurelsp_ls_dir = cls.ls_resources_dir() + clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings) clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir) if not os.path.exists(clojurelsp_executable_path): logger.log( diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 7d11a1df..88bf9bc4 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -25,6 +25,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency @@ -175,18 +176,20 @@ class CSharpLanguageServer(SolidLanguageServer): This is the official Roslyn-based language server from Microsoft. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - dotnet_path, language_server_path = self._ensure_server_installed(logger, config) + dotnet_path, language_server_path = self._ensure_server_installed(logger, config, solidlsp_settings) # Find solution or project file solution_or_project = find_solution_or_project_file(repository_root_path) # Create log directory - log_dir = Path(self.ls_resources_dir()) / "logs" + log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs" log_dir.mkdir(parents=True, exist_ok=True) # Build command using dotnet directly @@ -206,6 +209,7 @@ class CSharpLanguageServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", + solidlsp_settings, ) self.initialization_complete = threading.Event() @@ -215,15 +219,17 @@ class CSharpLanguageServer(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"] @classmethod - def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: + def _ensure_server_installed( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> tuple[str, str]: """ Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. Returns a tuple of (dotnet_path, language_server_dll_path). """ runtime_id = CSharpLanguageServer._get_runtime_id() lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id) - dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep) - server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep) + dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings) + server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings) return dotnet_path, server_dll_path @@ -261,8 +267,10 @@ class CSharpLanguageServer(SolidLanguageServer): return lang_server_dep, dotnet_runtime_dep - @staticmethod - def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: + @classmethod + def _ensure_dotnet_runtime( + cls, logger: LanguageServerLogger, 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 system_dotnet = shutil.which("dotnet") @@ -277,15 +285,17 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep) + return cls._ensure_dotnet_runtime_from_config(logger, runtime_dep, solidlsp_settings) @classmethod - def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> str: + def _ensure_language_server( + cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings + ) -> str: """Ensure language server is available and return the DLL path.""" package_name = lang_server_dep.package_name package_version = lang_server_dep.package_version - server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}" + server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}" server_dll = server_dir / lang_server_dep.binary_name if server_dll.exists(): @@ -294,10 +304,10 @@ class CSharpLanguageServer(SolidLanguageServer): # Download and install the language server logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO) - package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version) + package_path = cls._download_nuget_package_direct(logger, package_name, package_version, solidlsp_settings) # Extract and install - CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir) + cls._extract_language_server(lang_server_dep, package_path, server_dir) if not server_dll.exists(): raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") @@ -333,7 +343,9 @@ class CSharpLanguageServer(SolidLanguageServer): shutil.copytree(source_dir, server_dir, dirs_exist_ok=True) @classmethod - def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path: + def _download_nuget_package_direct( + cls, logger: LanguageServerLogger, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings + ) -> Path: """ Download a NuGet package directly from the Azure NuGet feed. Returns the path to the extracted package directory. @@ -341,7 +353,7 @@ class CSharpLanguageServer(SolidLanguageServer): azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" # Create temporary directory for package download - temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads" + temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads" temp_dir.mkdir(parents=True, exist_ok=True) try: @@ -390,7 +402,9 @@ class CSharpLanguageServer(SolidLanguageServer): ) from e @classmethod - def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str: + def _ensure_dotnet_runtime_from_config( + cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings + ) -> str: """ Ensure .NET 9 runtime is available using runtime dependency configuration. Returns the path to the dotnet executable. @@ -408,7 +422,7 @@ class CSharpLanguageServer(SolidLanguageServer): pass # Download .NET 9 runtime using config - dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0" + dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0" dotnet_exe = dotnet_dir / runtime_dep.binary_name if dotnet_exe.exists(): diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 28696ad6..c9cfcad1 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -6,6 +6,7 @@ import stat from solidlsp.ls import SolidLanguageServer from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -15,21 +16,22 @@ class DartLanguageServer(SolidLanguageServer): Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. """ - def __init__(self, config, logger, repository_root_path): + def __init__(self, config, logger, repository_root_path, solidlsp_settings: SolidLSPSettings): """ Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - executable_path = self._setup_runtime_dependencies(logger) + executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", + solidlsp_settings, ) @classmethod - def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger") -> str: + def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger", solidlsp_settings: SolidLSPSettings) -> str: deps = RuntimeDependencyCollection( [ RuntimeDependency( @@ -75,7 +77,7 @@ class DartLanguageServer(SolidLanguageServer): ] ) - dart_ls_dir = cls.ls_resources_dir() + dart_ls_dir = cls.ls_resources_dir(solidlsp_settings) dart_executable_path = deps.binary_path(dart_ls_dir) if not os.path.exists(dart_executable_path): diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 3ac80046..bf87aab7 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -51,7 +51,7 @@ class EclipseJDTLS(SolidLanguageServer): Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - runtime_dependency_paths = self._setupRuntimeDependencies(logger, config) + runtime_dependency_paths = self._setupRuntimeDependencies(logger, config, solidlsp_settings) self.runtime_dependency_paths = runtime_dependency_paths # ws_dir is the workspace directory for the EclipseJDTLS server @@ -157,7 +157,9 @@ class EclipseJDTLS(SolidLanguageServer): ] @classmethod - def _setupRuntimeDependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: + def _setupRuntimeDependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> RuntimeDependencyPaths: """ Setup runtime dependencies for EclipseJDTLS and return the paths. """ @@ -242,7 +244,7 @@ class EclipseJDTLS(SolidLanguageServer): gradle_path = str( PurePath( - cls.ls_resources_dir(), + cls.ls_resources_dir(solidlsp_settings), "gradle-7.3.3", ) ) @@ -258,7 +260,7 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(gradle_path) dependency = runtime_dependencies["vscode-java"][platformId.value] - vscode_java_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"])) + vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) os.makedirs(vscode_java_path, exist_ok=True) jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) @@ -287,7 +289,7 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(jdtls_readonly_config_path) dependency = runtime_dependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"])) + intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"])) os.makedirs(intellicode_directory_path, exist_ok=True) intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 16df9027..5f6a9794 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -14,6 +14,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from ..common import RuntimeDependency @@ -80,7 +81,9 @@ class ElixirTools(SolidLanguageServer): return None @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for Next LS. Downloads the Next LS binary for the current platform and returns the path to the executable. @@ -110,7 +113,7 @@ class ElixirTools(SolidLanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment" - next_ls_dir = os.path.join(cls.ls_resources_dir(), "next-ls") + next_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "next-ls") # Define runtime dependencies inline runtime_deps = { @@ -162,8 +165,10 @@ class ElixirTools(SolidLanguageServer): logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO) return executable_path - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): - nextls_executable_path = self._setup_runtime_dependencies(logger, config) + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): + nextls_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, @@ -171,6 +176,7 @@ class ElixirTools(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path), "elixir", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index 70eb15fa..c21663b7 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -11,6 +11,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class Gopls(SolidLanguageServer): @@ -70,7 +71,9 @@ class Gopls(SolidLanguageServer): return True - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): self._setup_runtime_dependency() super().__init__( @@ -79,6 +82,7 @@ class Gopls(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index f9c83976..2d1d48e2 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -16,6 +16,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -34,7 +35,9 @@ class Intelephense(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for Intelephense and return the command to start the server. """ @@ -58,7 +61,7 @@ class Intelephense(SolidLanguageServer): assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install intelephense if not already installed - intelephense_ls_dir = os.path.join(cls.ls_resources_dir(), "php-lsp") + intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp") os.makedirs(intelephense_ls_dir, exist_ok=True) intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") if not os.path.exists(intelephense_executable_path): @@ -79,11 +82,20 @@ class Intelephense(SolidLanguageServer): return f"{intelephense_executable_path} --stdio" - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): # Setup runtime dependencies before initializing - intelephense_cmd = self._setup_runtime_dependencies(logger, config) + intelephense_cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings) - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php") + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), + "php", + solidlsp_settings, + ) self.request_id = 0 @staticmethod diff --git a/src/solidlsp/language_servers/jedi_server.py b/src/solidlsp/language_servers/jedi_server.py index 161ce754..956e00b9 100644 --- a/src/solidlsp/language_servers/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_server.py @@ -13,6 +13,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class JediServer(SolidLanguageServer): @@ -20,7 +21,9 @@ class JediServer(SolidLanguageServer): Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -30,6 +33,7 @@ class JediServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), "python", + solidlsp_settings, ) @override diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index ef2a77ca..5d0d2083 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -14,6 +14,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings @dataclasses.dataclass @@ -32,11 +33,13 @@ class KotlinLanguageServer(SolidLanguageServer): Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - runtime_dependency_paths = self._setup_runtime_dependencies(logger, config) + runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings) self.runtime_dependency_paths = runtime_dependency_paths # Create command to execute the Kotlin Language Server script @@ -51,10 +54,13 @@ class KotlinLanguageServer(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), "kotlin", + solidlsp_settings, ) @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> KotlinRuntimeDependencyPaths: """ Setup runtime dependencies for Kotlin Language Server and return the paths. """ @@ -111,7 +117,7 @@ class KotlinLanguageServer(SolidLanguageServer): java_dependency = runtime_dependencies["java"][platform_id.value] # Setup paths for dependencies - static_dir = os.path.join(cls.ls_resources_dir(), "kotlin_language_server") + static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "kotlin_language_server") os.makedirs(static_dir, exist_ok=True) # Setup Java paths diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index eafaa310..a23a58a8 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -19,6 +19,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings def breadth_first_file_scan(root) -> Iterable[str]: @@ -59,11 +60,13 @@ class OmniSharp(SolidLanguageServer): Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config) + omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: @@ -102,7 +105,9 @@ class OmniSharp(SolidLanguageServer): "formattingOptions:indentationSize=4", ] ) - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp") + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings + ) self.server_ready = threading.Event() self.definition_available = threading.Event() @@ -138,7 +143,9 @@ class OmniSharp(SolidLanguageServer): return d @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> tuple[str, str]: """ Setup runtime dependencies for OmniSharp. """ @@ -181,7 +188,7 @@ class OmniSharp(SolidLanguageServer): assert "OmniSharp" in runtime_dependencies assert "RazorOmnisharp" in runtime_dependencies - omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "OmniSharp") + omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp") if not os.path.exists(omnisharp_ls_dir): os.makedirs(omnisharp_ls_dir) FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip") @@ -189,7 +196,7 @@ class OmniSharp(SolidLanguageServer): assert os.path.exists(omnisharp_executable_path) os.chmod(omnisharp_executable_path, stat.S_IEXEC) - razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "RazorOmnisharp") + razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp") if not os.path.exists(razor_omnisharp_ls_dir): os.makedirs(razor_omnisharp_ls_dir) FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip") diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 905bb3d6..c760ebc1 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class PyrightServer(SolidLanguageServer): @@ -23,7 +24,9 @@ class PyrightServer(SolidLanguageServer): Contains various configurations and settings specific to Python. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -36,6 +39,7 @@ class PyrightServer(SolidLanguageServer): # Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), "python", + solidlsp_settings, ) # Event to signal when initial workspace analysis is complete diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py index 0f2f2cf1..48bae28a 100644 --- a/src/solidlsp/language_servers/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -24,17 +25,20 @@ class RustAnalyzer(SolidLanguageServer): Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config) + rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), "rust", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() @@ -46,7 +50,9 @@ class RustAnalyzer(SolidLanguageServer): return super().is_ignored_dirname(dirname) or dirname in ["target"] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for rust_analyzer and return the command to start the server. """ @@ -92,7 +98,7 @@ class RustAnalyzer(SolidLanguageServer): # "win-x64", # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" - rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(), "RustAnalyzer") + rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RustAnalyzer") rustanalyzer_executable_path = deps.binary_path(rustanalyzer_ls_dir) if not os.path.exists(rustanalyzer_ls_dir): os.makedirs(rustanalyzer_ls_dir) diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 8034ad80..56a1b7f7 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -18,6 +18,7 @@ from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings class Solargraph(SolidLanguageServer): @@ -26,7 +27,9 @@ class Solargraph(SolidLanguageServer): Contains various configurations and settings specific to Ruby. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a Solargraph instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -38,6 +41,7 @@ class Solargraph(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), "ruby", + solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index 5a230a23..e40d9af1 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -12,6 +12,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -57,7 +58,7 @@ class TerraformLS(SolidLanguageServer): ) @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger) -> str: + def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for terraform-ls. Downloads and installs terraform-ls if not already present. @@ -102,10 +103,10 @@ class TerraformLS(SolidLanguageServer): ) dependency = deps.single_for_current_platform() - terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir()) + terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings)) if not os.path.exists(terraform_ls_executable_path): logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO) - deps.install(logger, cls.ls_resources_dir()) + deps.install(logger, cls.ls_resources_dir(solidlsp_settings)) assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" @@ -115,11 +116,13 @@ class TerraformLS(SolidLanguageServer): return terraform_ls_executable_path - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - terraform_ls_executable_path = self._setup_runtime_dependencies(logger) + terraform_ls_executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings) super().__init__( config, @@ -127,6 +130,7 @@ class TerraformLS(SolidLanguageServer): repository_root_path, ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), "terraform", + solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index f994f95f..9709cc8f 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -17,6 +17,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -41,17 +42,20 @@ class TypeScriptLanguageServer(SolidLanguageServer): Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config) + ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), "typescript", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @@ -66,7 +70,9 @@ class TypeScriptLanguageServer(SolidLanguageServer): ] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for TypeScript Language Server and return the command to start the server. """ @@ -113,7 +119,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install typescript and typescript-language-server if not already installed - tsserver_ls_dir = os.path.join(cls.ls_resources_dir(), "ts-lsp") + tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp") tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") if not os.path.exists(tsserver_executable_path): logger.log(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...", logging.INFO) diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index 40de7b55..c8596b89 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -19,6 +19,7 @@ from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection @@ -29,17 +30,20 @@ class VtsLanguageServer(SolidLanguageServer): Contains various configurations and settings specific to TypeScript via vtsls wrapper. """ - def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + def __init__( + self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings + ): """ Creates a VtsLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config) + vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=vts_lsp_executable_path, cwd=repository_root_path), "typescript", + solidlsp_settings, ) self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() @@ -54,7 +58,9 @@ class VtsLanguageServer(SolidLanguageServer): ] @classmethod - def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + def _setup_runtime_dependencies( + cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings + ) -> str: """ Setup runtime dependencies for VTS Language Server and return the command to start the server. """ @@ -81,7 +87,7 @@ class VtsLanguageServer(SolidLanguageServer): ), ] ) - vts_ls_dir = os.path.join(cls.ls_resources_dir(), "vts-lsp") + vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp") vts_executable_path = os.path.join(vts_ls_dir, "vtsls") # Verify both node and npm are installed diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index e47e83ca..873213e0 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -119,8 +119,8 @@ class SolidLanguageServer(ABC): config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, - solidlsp_settings: SolidLSPSettings, timeout: float | None = None, + solidlsp_settings: SolidLSPSettings | None = None, ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -135,6 +135,8 @@ class SolidLanguageServer(ABC): :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 ( diff --git a/test/conftest.py b/test/conftest.py index 90e522d0..849af543 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,11 +4,13 @@ from pathlib import Path import pytest from sensai.util.logging import configure +from serena.constants import SERENA_MANAGED_DIR_IN_HOME from serena.project import Project from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.settings import SolidLSPSettings configure(level=logging.ERROR) @@ -43,7 +45,9 @@ def create_ls( ignored_paths.extend(spec.patterns) config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) logger = LanguageServerLogger(log_level=log_level) - return SolidLanguageServer.create(config, logger, repo_path) + return SolidLanguageServer.create( + config, logger, repo_path, solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME) + ) def create_default_ls(language: Language) -> SolidLanguageServer: diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 59f393ac..89d3caea 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -13,6 +13,7 @@ from solidlsp.language_servers.csharp_language_server import ( ) from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import SymbolUtils +from solidlsp.settings import SolidLSPSettings @pytest.mark.csharp @@ -214,7 +215,9 @@ class TestCSharpSolutionProjectOpening: mock_config.ignored_paths = [] # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + mock_settings = Mock(spec=SolidLSPSettings) + mock_settings.ls_resources_dir = "/tmp/test_ls_resources" + CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings) # Verify that logger was called with solution file discovery mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO @@ -236,7 +239,9 @@ class TestCSharpSolutionProjectOpening: mock_config.ignored_paths = [] # Create CSharpLanguageServer instance - CSharpLanguageServer(mock_config, mock_logger, str(temp_path)) + mock_settings = Mock(spec=SolidLSPSettings) + mock_settings.ls_resources_dir = "/tmp/test_ls_resources" + CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings) # Verify that logger was called with warning about no solution/project files mock_logger.log.assert_any_call( From 4b69a0ddab819e729c5f880eaccf57403a13881d Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Fri, 25 Jul 2025 15:58:36 +0200 Subject: [PATCH 07/49] Minor cleanup [ci skip] --- src/solidlsp/ls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 873213e0..415f4139 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -96,8 +96,6 @@ class SolidLanguageServer(ABC): Returns the directory where the language server resources are downloaded. This is used to store language server binaries, configuration files, etc. """ - if solidlsp_settings is None: - solidlsp_settings = SolidLSPSettings() result = os.path.join(solidlsp_settings.ls_resources_dir, cls.__name__) # Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home From 9c83cf74f4dc7b7b9d4d8a20dc0b62e13a83a5b4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 11:00:40 +0200 Subject: [PATCH 08/49] Further improve logging of dependency installation --- src/solidlsp/language_servers/common.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 58a1f1b9..4ce8f3b4 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -80,7 +80,7 @@ class RuntimeDependencyCollection: kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name log.info("Running command '%s' in '%s'", command, cwd) - completed_proces = subprocess.run( + completed_process = subprocess.run( command, shell=True, check=True, @@ -89,11 +89,13 @@ class RuntimeDependencyCollection: stderr=subprocess.STDOUT, **kwargs, ) - log.log( - logging.WARNING if completed_proces.returncode else logging.INFO, - "Command completed with return code %d", - completed_proces.returncode, - ) + if completed_process.returncode != 0: + log.warning("Command '%s' failed with return code %d", command, completed_process.returncode) + log.warning("Command output:\n%s", completed_process.stdout) + else: + log.info( + "Command completed successfully", + ) @staticmethod def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: From a8078bbc5230cb059ff04b45fe28ee8ad1d8c518 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 27 Jul 2025 16:01:03 +0700 Subject: [PATCH 09/49] docs(readme): fix wrong sse url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a349aef1..788ac9b6 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ you would run this command from the Serena directory, uv run serena-mcp-server --transport sse --port 9121 ``` -and then configure your client to connect to `http://localhost/sse:9121`. +and then configure your client to connect to `http://localhost:9121/sse`. #### Command-Line Arguments From f55eb0c9b438beed1bae87d19d961bb2de88855e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:26:38 +0200 Subject: [PATCH 10/49] Add type annotations --- src/serena/config/serena_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 839f2f40..41ef6433 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -47,13 +47,13 @@ class SerenaPaths: Provides paths to various Serena-related directories and files. """ - def __init__(self): + def __init__(self) -> None: self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME """ the path to the user's Serena configuration directory, which is typically ~/.serena """ - def get_next_log_file_path(self, prefix: str): + def get_next_log_file_path(self, prefix: str) -> str: """ :param prefix: the filename prefix indicating the type of the log file :return: the full path to the log file to use From 0e890407182ed1cc51710539a68228d2a9b9f922 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:25:07 +0200 Subject: [PATCH 11/49] Use strategy pattern instead of higher-order function in LanguageServerRequest --- src/solidlsp/ls_handler.py | 2 +- src/solidlsp/ls_request.py | 112 +++++++++++++++++++------------------ 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 731952f3..9fd65823 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -104,7 +104,7 @@ class SolidLanguageServerHandler: start_independent_lsp_process=True, request_timeout: float | None = None, ) -> None: - self.send = LanguageServerRequest(self.send_request) + self.send = LanguageServerRequest(self) self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info diff --git a/src/solidlsp/ls_request.py b/src/solidlsp/ls_request.py index 209a3f04..f81727d4 100644 --- a/src/solidlsp/ls_request.py +++ b/src/solidlsp/ls_request.py @@ -1,11 +1,15 @@ -from typing import Union +from typing import Any, Union +from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.lsp_protocol_handler import lsp_types class LanguageServerRequest: - def __init__(self, send_request): - self.send_request = send_request + def __init__(self, handler: SolidLanguageServerHandler): + self.handler = handler + + def _send_request(self, method: str, params: Any | None = None) -> Any: + return self.handler.send_request(method, params) def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text @@ -13,7 +17,7 @@ class LanguageServerRequest: (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such. """ - return self.send_request("textDocument/implementation", params) + return self._send_request("textDocument/implementation", params) def type_definition( self, params: lsp_types.TypeDefinitionParams @@ -23,7 +27,7 @@ class LanguageServerRequest: (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such. """ - return self.send_request("textDocument/typeDefinition", params) + return self._send_request("textDocument/typeDefinition", params) def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's @@ -31,7 +35,7 @@ class LanguageServerRequest: response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentColor", params) + return self._send_request("textDocument/documentColor", params) def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's @@ -39,7 +43,7 @@ class LanguageServerRequest: response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/colorPresentation", params) + return self._send_request("textDocument/colorPresentation", params) def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: """A request to provide folding ranges in a document. The request's @@ -47,7 +51,7 @@ class LanguageServerRequest: response is of type {@link FoldingRangeList} or a Thenable that resolves to such. """ - return self.send_request("textDocument/foldingRange", params) + return self._send_request("textDocument/foldingRange", params) def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text @@ -56,7 +60,7 @@ class LanguageServerRequest: or a typed array of {@link DeclarationLink} or a Thenable that resolves to such. """ - return self.send_request("textDocument/declaration", params) + return self._send_request("textDocument/declaration", params) def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: """A request to provide selection ranges in a document. The request's @@ -64,7 +68,7 @@ class LanguageServerRequest: response is of type {@link SelectionRange SelectionRange[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/selectionRange", params) + return self._send_request("textDocument/selectionRange", params) def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: """A request to result a `CallHierarchyItem` in a document at a given position. @@ -72,42 +76,42 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("textDocument/prepareCallHierarchy", params) + return self._send_request("textDocument/prepareCallHierarchy", params) def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None: """A request to resolve the incoming calls for a given `CallHierarchyItem`. @since 3.16.0 """ - return self.send_request("callHierarchy/incomingCalls", params) + return self._send_request("callHierarchy/incomingCalls", params) def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. @since 3.16.0 """ - return self.send_request("callHierarchy/outgoingCalls", params) + return self._send_request("callHierarchy/outgoingCalls", params) def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/full", params) + return self._send_request("textDocument/semanticTokens/full", params) def semantic_tokens_delta( self, params: lsp_types.SemanticTokensDeltaParams ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/full/delta", params) + return self._send_request("textDocument/semanticTokens/full/delta", params) def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/range", params) + return self._send_request("textDocument/semanticTokens/range", params) def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. @since 3.16.0 """ - return self.send_request("textDocument/linkedEditingRange", params) + return self._send_request("textDocument/linkedEditingRange", params) def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually @@ -115,7 +119,7 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willCreateFiles", params) + return self._send_request("workspace/willCreateFiles", params) def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually @@ -123,7 +127,7 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willRenameFiles", params) + return self._send_request("workspace/willRenameFiles", params) def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when @@ -131,14 +135,14 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willDeleteFiles", params) + return self._send_request("workspace/willDeleteFiles", params) def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. The response is of type {@link Moniker Moniker[]} or `null`. """ - return self.send_request("textDocument/moniker", params) + return self._send_request("textDocument/moniker", params) def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to result a `TypeHierarchyItem` in a document at a given position. @@ -146,21 +150,21 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/prepareTypeHierarchy", params) + return self._send_request("textDocument/prepareTypeHierarchy", params) def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the supertypes for a given `TypeHierarchyItem`. @since 3.17.0 """ - return self.send_request("typeHierarchy/supertypes", params) + return self._send_request("typeHierarchy/supertypes", params) def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the subtypes for a given `TypeHierarchyItem`. @since 3.17.0 """ - return self.send_request("typeHierarchy/subtypes", params) + return self._send_request("typeHierarchy/subtypes", params) def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: """A request to provide inline values in a document. The request's parameter is of @@ -169,7 +173,7 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/inlineValue", params) + return self._send_request("textDocument/inlineValue", params) def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: """A request to provide inlay hints in a document. The request's parameter is of @@ -178,7 +182,7 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/inlayHint", params) + return self._send_request("textDocument/inlayHint", params) def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. @@ -187,21 +191,21 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("inlayHint/resolve", params) + return self._send_request("inlayHint/resolve", params) def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. @since 3.17.0 """ - return self.send_request("textDocument/diagnostic", params) + return self._send_request("textDocument/diagnostic", params) def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. @since 3.17.0 """ - return self.send_request("workspace/diagnostic", params) + return self._send_request("workspace/diagnostic", params) def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. @@ -210,7 +214,7 @@ class LanguageServerRequest: the response if of type {@link InitializeResult} of a Thenable that resolves to such. """ - return self.send_request("initialize", params) + return self._send_request("initialize", params) def shutdown(self) -> None: """A shutdown request is sent from the client to the server. @@ -218,7 +222,7 @@ class LanguageServerRequest: server. The only notification that is sent after a shutdown request is the exit event. """ - return self.send_request("shutdown") + return self._send_request("shutdown") def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: """A document will save request is sent from the client to the server before @@ -228,7 +232,7 @@ class LanguageServerRequest: server constantly fails on this request. This is done to keep the save fast and reliable. """ - return self.send_request("textDocument/willSaveWaitUntil", params) + return self._send_request("textDocument/willSaveWaitUntil", params) def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's @@ -241,24 +245,24 @@ class LanguageServerRequest: request. However, properties that are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `textEdit`, must not be changed during resolve. """ - return self.send_request("textDocument/completion", params) + return self._send_request("textDocument/completion", params) def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response is of type {@link CompletionItem} or a Thenable that resolves to such. """ - return self.send_request("completionItem/resolve", params) + return self._send_request("completionItem/resolve", params) def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link Hover} or a Thenable that resolves to such. """ - return self.send_request("textDocument/hover", params) + return self._send_request("textDocument/hover", params) def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: - return self.send_request("textDocument/signatureHelp", params) + return self._send_request("textDocument/signatureHelp", params) def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text @@ -267,7 +271,7 @@ class LanguageServerRequest: or a typed array of {@link DefinitionLink} or a Thenable that resolves to such. """ - return self.send_request("textDocument/definition", params) + return self._send_request("textDocument/definition", params) def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: """A request to resolve project-wide references for the symbol denoted @@ -275,7 +279,7 @@ class LanguageServerRequest: type {@link ReferenceParams} the response is of type {@link Location Location[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/references", params) + return self._send_request("textDocument/references", params) def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: """Request to resolve a {@link DocumentHighlight} for a given @@ -283,7 +287,7 @@ class LanguageServerRequest: (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] (#DocumentHighlight) or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentHighlight", params) + return self._send_request("textDocument/documentHighlight", params) def document_symbol( self, params: lsp_types.DocumentSymbolParams @@ -293,18 +297,18 @@ class LanguageServerRequest: response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentSymbol", params) + return self._send_request("textDocument/documentSymbol", params) def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: """A request to provide commands for the given text document and range.""" - return self.send_request("textDocument/codeAction", params) + return self._send_request("textDocument/codeAction", params) def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response is of type {@link CodeAction} or a Thenable that resolves to such. """ - return self.send_request("codeAction/resolve", params) + return self._send_request("codeAction/resolve", params) def workspace_symbol( self, params: lsp_types.WorkspaceSymbolParams @@ -318,7 +322,7 @@ class LanguageServerRequest: need to advertise support for WorkspaceSymbols via the client capability `workspace.symbol.resolveSupport`. """ - return self.send_request("workspace/symbol", params) + return self._send_request("workspace/symbol", params) def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace @@ -326,52 +330,52 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("workspaceSymbol/resolve", params) + return self._send_request("workspaceSymbol/resolve", params) def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: """A request to provide code lens for the given text document.""" - return self.send_request("textDocument/codeLens", params) + return self._send_request("textDocument/codeLens", params) def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" - return self.send_request("codeLens/resolve", params) + return self._send_request("codeLens/resolve", params) def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: """A request to provide document links""" - return self.send_request("textDocument/documentLink", params) + return self._send_request("textDocument/documentLink", params) def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response is of type {@link DocumentLink} or a Thenable that resolves to such. """ - return self.send_request("documentLink/resolve", params) + return self._send_request("documentLink/resolve", params) def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a whole document.""" - return self.send_request("textDocument/formatting", params) + return self._send_request("textDocument/formatting", params) def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a range in a document.""" - return self.send_request("textDocument/rangeFormatting", params) + return self._send_request("textDocument/rangeFormatting", params) def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to format a document on type.""" - return self.send_request("textDocument/onTypeFormatting", params) + return self._send_request("textDocument/onTypeFormatting", params) def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" - return self.send_request("textDocument/rename", params) + return self._send_request("textDocument/rename", params) def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. @since 3.16 - support for default behavior """ - return self.send_request("textDocument/prepareRename", params) + return self._send_request("textDocument/prepareRename", params) def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return a workspace edit which the client will apply to the workspace. """ - return self.send_request("workspace/executeCommand", params) + return self._send_request("workspace/executeCommand", params) From 6fbea2dfe966ac26918f953751938bb5fdc40753 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:31:45 +0200 Subject: [PATCH 12/49] Remove obsolete termination handling --- src/solidlsp/ls_handler.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 9fd65823..5e6754be 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -225,21 +225,6 @@ class SolidLanguageServerHandler: # First try to terminate the process tree gracefully self._signal_process_tree(process, terminate=True) - # TODO - """ - # Wait for the process to exit (with timeout) - try: - asyncio.wait_for(process.wait(), timeout=10) - except (asyncio.TimeoutError, Exception): - # If termination failed, forcefully kill the process tree - self._signal_process_tree(process, terminate=False) - try: - # Give it one more chance to exit - await asyncio.wait_for(process.wait(), timeout=2) - except Exception: - pass - """ - def _signal_process_tree(self, process, terminate=True): """Send signal (terminate or kill) to the process and all its children.""" signal_method = "terminate" if terminate else "kill" @@ -283,14 +268,6 @@ class SolidLanguageServerHandler: self._log("Sending exit notification to server") self.notify.exit() self._log("Sent exit notification to server") - # TODO - """ - if self.process and self.process.stdout: - self.process.stdout.set_exception(StopLoopException()) - # This yields the control to the event loop to allow the exception to be handled - # in the run_forever and run_forever_stderr methods - await asyncio.sleep(0) - """ def _log(self, message: str | StringDict) -> None: """ From 0aaeaef64cfdce1be1663f5b2e02f0c39e8aa070 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 27 Jul 2025 15:16:57 +0200 Subject: [PATCH 13/49] Updated mcp dependency --- uv.lock | 130 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/uv.lock b/uv.lock index 52fe9264..4322e294 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = "==3.11.*" [[package]] name = "agno" -version = "1.7.2" +version = "1.7.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -20,9 +20,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/64/6a82d2375145fd733009db0ada531b595fc2f3c1712ac91c91b2e5fcf02f/agno-1.7.2.tar.gz", hash = "sha256:f4f97e5ea1f9c0781d9bb8396862c82671381386c1569cdb456d70f6c8d3a108", size = 661788, upload-time = "2025-07-10T15:50:06.217Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/84/d2390249d1b0150971a749df79cf653a631227b748fe10be1ef4f426ed2d/agno-1.7.6.tar.gz", hash = "sha256:8df575f86eac6511996045fb90d281ca61fd19774dccbb82f9dc34bda46effdf", size = 724233, upload-time = "2025-07-24T14:36:27.356Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/24/884cbd1133e3b9985d53733250251098caead365bb8706af8da105813abe/agno-1.7.2-py3-none-any.whl", hash = "sha256:37a1985e67762f0d8016318ba5d8b8697c855827d7c86b21a41cdff088294cc2", size = 888229, upload-time = "2025-07-10T15:50:04.262Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7e/5f85c92a34f41f8ffb1467768bae50506f734320c478ed24c4237dc342ea/agno-1.7.6-py3-none-any.whl", hash = "sha256:c6350e44747b37048d4ac080ffa5fb0e0ce2b3d5bc55d37120711ae8d31f27c6", size = 962755, upload-time = "2025-07-24T14:36:25.648Z" }, ] [[package]] @@ -36,7 +36,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.57.1" +version = "0.59.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -47,9 +47,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/75/6261a1a8d92aed47e27d2fcfb3a411af73b1435e6ae1186da02b760565d0/anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0", size = 423775, upload-time = "2025-07-03T16:57:35.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/52daff015f5a1f24eec891b3041f5f816712fea8b5113dc76638bcbc23d8/anthropic-0.59.0.tar.gz", hash = "sha256:d710d1ef0547ebbb64b03f219e44ba078e83fc83752b96a9b22e9726b523fd8f", size = 425679, upload-time = "2025-07-23T16:23:16.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/cf/ca0ba77805aec6171629a8b665c7dc224dab374539c3d27005b5d8c100a0/anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306", size = 292779, upload-time = "2025-07-03T16:57:34.636Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/03f680393eac04afd8f2be44ee0e39e033c40faf43dbc1c11764b07a2687/anthropic-0.59.0-py3-none-any.whl", hash = "sha256:cbc8b3dccef66ad6435c4fa1d317e5ebb092399a4b88b33a09dc4bf3944c3183", size = 293057, upload-time = "2025-07-23T16:23:14.934Z" }, ] [[package]] @@ -130,11 +130,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.9" +version = "2025.7.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" }, + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, ] [[package]] @@ -200,11 +200,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.16" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] [[package]] @@ -267,14 +267,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.44" +version = "3.1.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, ] [[package]] @@ -293,7 +293,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.25.0" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -305,9 +305,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/59/c9b9148c8702b60253f5a251f16ae436534c5d4362da193c9db05ac9858c/google_genai-1.25.0.tar.gz", hash = "sha256:a08a79c819a5d949d9948cd372e36e512bf85cd28158994daaa36d0ec4cb2b02", size = 228141, upload-time = "2025-07-09T20:53:47.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/37/6c0ececc3a7a629029b5beed2ceb9f28f73292236eb96272355636769b0d/google_genai-1.27.0.tar.gz", hash = "sha256:15a13ffe7b3938da50b9ab77204664d82122617256f55b5ce403d593848ef635", size = 220099, upload-time = "2025-07-23T22:00:46.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/ec/149f3d49b56cf848142071772aabb1c290b535bd9b5327a5dfccf1d00332/google_genai-1.25.0-py3-none-any.whl", hash = "sha256:fb5cee79b9a0a1b2afd5cfdf279099ecebd186551eefcaa6ec0c6016244e6138", size = 226847, upload-time = "2025-07-09T20:53:46.532Z" }, + { url = "https://files.pythonhosted.org/packages/5a/12/279afe7357af73f9737a3412b6f0bc1482075b896340eb46a2f9cb0fd791/google_genai-1.27.0-py3-none-any.whl", hash = "sha256:afd6b4efaf8ec1d20a6e6657d768b68d998d60007c6e220e9024e23c913c1833", size = 218489, upload-time = "2025-07-23T22:00:44.879Z" }, ] [[package]] @@ -489,7 +489,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.24.0" +version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -497,9 +497,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] [[package]] @@ -558,7 +558,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.11.0" +version = "1.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -573,9 +573,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/85/f36d538b1286b7758f35c1b69d93f2719d2df90c01bd074eadd35f6afc35/mcp-1.12.2.tar.gz", hash = "sha256:a4b7c742c50ce6ed6d6a6c096cca0e3893f5aecc89a59ed06d47c4e6ba41edcc", size = 426202, upload-time = "2025-07-24T18:29:05.175Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cf/3fd38cfe43962452e4bfadc6966b2ea0afaf8e0286cb3991c247c8c33ebd/mcp-1.12.2-py3-none-any.whl", hash = "sha256:b86d584bb60193a42bd78aef01882c5c42d614e416cbf0480149839377ab5a5f", size = 158473, upload-time = "2025-07-24T18:29:03.419Z" }, ] [[package]] @@ -589,22 +589,22 @@ wheels = [ [[package]] name = "mypy" -version = "1.16.1" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, - { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, ] [[package]] @@ -913,12 +913,12 @@ wheels = [ [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, ] [[package]] @@ -992,15 +992,15 @@ wheels = [ [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, ] [[package]] @@ -1079,27 +1079,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.3" +version = "0.12.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, - { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, - { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, - { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, - { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, - { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, - { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, - { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, - { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, - { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, + { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, + { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, + { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, + { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, ] [[package]] @@ -1250,14 +1250,14 @@ wheels = [ [[package]] name = "sse-starlette" -version = "2.4.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/3e/eae74d8d33e3262bae0a7e023bb43d8bdd27980aa3557333f4632611151f/sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926", size = 18635, upload-time = "2025-07-06T09:41:33.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f1/6c7eaa8187ba789a6dd6d74430307478d2a91c23a5452ab339b6fbe15a08/sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a", size = 10824, upload-time = "2025-07-06T09:41:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] [[package]] @@ -1276,15 +1276,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.47.1" +version = "0.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] [[package]] From 770e3658eb35fbbb1657c3ab4a3e23f486a21baa Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 27 Jul 2025 15:17:34 +0200 Subject: [PATCH 14/49] MCP: Pass initial instructions to fastmcp constructor --- src/serena/mcp.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index ff8a11ba..f5f785e0 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -172,7 +172,8 @@ class SerenaMCPFactory: Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port) - mcp = FastMCP(**mcp_settings.model_dump()) + instructions = self._get_initial_instructions() + mcp = FastMCP(**mcp_settings.model_dump(), instructions=instructions) return mcp @asynccontextmanager @@ -181,6 +182,10 @@ class SerenaMCPFactory: """Manage server startup and shutdown lifecycle.""" yield None # ensures MyPy understands we yield None + @abstractmethod + def _get_initial_instructions(self) -> str: + pass + class SerenaMCPFactorySingleProcess(SerenaMCPFactory): """ @@ -207,6 +212,11 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory): assert self.agent is not None yield from self.agent.get_exposed_tool_instances() + def _get_initial_instructions(self) -> str: + assert self.agent is not None + # we don't use the tool (which at the time of writing calls this method), since the tool may be disabled by the config + return self.agent.create_system_prompt() + @asynccontextmanager async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: self._set_mcp_tools(mcp_server) From f30005fe4bee4854872b2ff10fe563b9a9f743f0 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 27 Jul 2025 15:22:17 +0200 Subject: [PATCH 15/49] MCP: Updated readme on using initial instructions --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 788ac9b6..24abe333 100644 --- a/README.md +++ b/README.md @@ -354,11 +354,14 @@ For example, when using `uvx`, you would run claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) ``` -ℹ️ Serena comes with an instruction text, and Claude needs to read it to properly use Serena's tools. - Once in Claude Code, you can ask to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text. - Do this whenever you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools. +ℹ️ Serena comes with an instruction text, and Claude needs to read it to properly use Serena's tools. + As of version `v1.0.52`, claude code reads the instructions of the MCP server, so this is handled automatically. + If you are using an older version, or if Claude fails to read the instructions, you can ask it explicitly + to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text. + You may have to do that whenever you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools. -ℹ️ **NEW**: an alternative to the above is adding the instructions as part of the system prompt, then you will not need to run the command above or to remember re-running it after compacting. +ℹ️ If Claude is not automatically picking up on the instructions and running Serena's tools, and you don't want to run the command in each session, + an alternative to the above is adding the instructions as part of the system prompt, then you will not need to run the command above or to remember re-running it after compacting. This can be achieved through starting claude code with `claude --append-system-prompt $(uvx --from git+https://github.com/oraios/serena serena print-system-prompt)`. Note that this is **experimental**, Claude may not understand the instructions correctly in this way, and we haven't thoroughly tested the resulting behavior. Please report any issues you encounter. From a15833b22d3790927894a3717f04fa8d31ce7c44 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 18:24:03 +0200 Subject: [PATCH 16/49] Improve error handling in language server communication * Introduce appropriate exceptions that allow errors to be sufficiently identified * If an error occurs in the read thread of the LS output stream, cancel all pending requests, setting an appropriate error * Propagate error information all the way to the original caller (in the tool code): If errors are due to LS termination, restart the server and retry --- src/serena/tools/tools_base.py | 22 ++++--- src/solidlsp/ls_exceptions.py | 37 +++++++++-- src/solidlsp/ls_handler.py | 111 +++++++++++++++++++++++---------- src/solidlsp/ls_request.py | 8 ++- 4 files changed, 128 insertions(+), 50 deletions(-) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 173726fc..c8163390 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -1,6 +1,5 @@ import inspect import os -import traceback from abc import ABC from collections.abc import Iterable from dataclasses import dataclass @@ -16,6 +15,7 @@ from serena.prompt_factory import PromptFactory from serena.symbol import LanguageServerSymbolRetriever from serena.util.class_decorators import singleton from serena.util.inspection import iter_subclasses +from solidlsp.ls_exceptions import LanguageServerException if TYPE_CHECKING: from serena.agent import LinesRead, MemoriesManager, SerenaAgent @@ -237,18 +237,24 @@ class Tool(Component): self.agent.reset_language_server() # apply the actual tool - result = apply_fn(**kwargs) + try: + result = apply_fn(**kwargs) + except LanguageServerException as e: + if e.is_language_server_terminated(): + log.error(f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ...") + self.agent.reset_language_server() + result = apply_fn(**kwargs) + else: + raise + + # record tool usage self.agent.record_tool_usage_if_enabled(kwargs, result, self) except Exception as e: if not catch_exceptions: raise - msg = f"Error executing tool: {e}\n{traceback.format_exc()}" - log.error( - f"Error executing tool: {e}. " - f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", - exc_info=e, - ) + msg = f"Error executing tool: {e}" + log.error(f"Error executing tool: {e}", exc_info=e) result = msg if log_call: diff --git a/src/solidlsp/ls_exceptions.py b/src/solidlsp/ls_exceptions.py index 0484da6b..a3bb0f11 100644 --- a/src/solidlsp/ls_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -1,15 +1,40 @@ """ -This module contains the exceptions raised by the Multilspy framework. +This module contains the exceptions raised by the framework. """ class LanguageServerException(Exception): - """ - Exceptions raised by the Multilspy framework. - """ - - def __init__(self, message: str): + def __init__(self, message: str, cause: Exception | None = None): """ Initializes the exception with the given message. + + :param message: the message describing the exception + :param cause: the original exception that caused this exception, if any. + For exceptions raised during request handling, this is typically + * an LSPError for errors returned by the LSP server + * LanguageServerTerminatedException for errors due to the language server having terminated. """ + self.cause = cause super().__init__(message) + + def is_language_server_terminated(self): + """ + :return: True if the exception is caused by the language server having terminated as indicated + by the causing exception being an instance of LanguageServerTerminatedException. + """ + from .ls_handler import LanguageServerTerminatedException + + return isinstance(self.cause, LanguageServerTerminatedException) + + def __str__(self) -> str: + """ + Returns a string representation of the exception. + """ + s = super().__str__() + if self.cause: + if "\n" in s: + s += "\n" + else: + s += " " + s += f"(caused by {self.cause})" + return s diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 5e6754be..ab8a6772 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -12,6 +12,7 @@ from queue import Queue from typing import Any import psutil +from sensai.util.string import ToStringMixin from solidlsp.ls_exceptions import LanguageServerException from solidlsp.ls_request import LanguageServerRequest @@ -35,23 +36,50 @@ from solidlsp.lsp_protocol_handler.server import ( log = logging.getLogger(__name__) -class Request: +class LanguageServerTerminatedException(Exception): + """ + Exception raised when the language server process has terminated unexpectedly. + """ + + def __init__(self, message: str, cause: Exception | None = None) -> None: + super().__init__(message) + self.message = message + self.cause = cause + + def __str__(self) -> str: + return f"LanguageServerTerminatedException: {self.message}" + (f"; Cause: {self.cause}" if self.cause else "") + + +class Request(ToStringMixin): @dataclass class Result: payload: PayloadLike | None = None - error: Error | None = None + error: Exception | None = None def is_error(self) -> bool: return self.error is not None - def __init__(self) -> None: + def __init__(self, request_id: int, method: str) -> None: + self._request_id = request_id + self._method = method + self._status = "pending" self._result_queue = Queue() + def _tostring_includes(self) -> list[str]: + return ["_request_id", "_status", "_method"] + def on_result(self, params: PayloadLike) -> None: + self._status = "completed" self._result_queue.put(Request.Result(payload=params)) - def on_error(self, err: Error) -> None: + def on_error(self, err: Exception) -> None: + """ + :param err: the error that occurred while processing the request (typically an LSPError + for errors returned by the LS or LanguageServerTerminatedException if the error + is due to the language server process terminating unexpectedly). + """ + self._status = "error" self._result_queue.put(Request.Result(error=err)) def get_result(self, timeout: float | None = None) -> Result: @@ -75,10 +103,8 @@ class SolidLanguageServerHandler: notify: A LspNotification object that can be used to send notifications to the server. cmd: A string that represents the command to launch the language server process. process: A subprocess.Popen object that represents the language server process. - _received_shutdown: A boolean flag that indicates whether the client has received - a shutdown request from the server. request_id: An integer that represents the next available request id for the client. - _response_handlers: A dictionary that maps request ids to Request objects that + _pending_requests: A dictionary that maps request ids to Request objects that store the results or errors of the requests. on_request_handlers: A dictionary that maps method names to callback functions that handle requests from the server. @@ -109,10 +135,10 @@ class SolidLanguageServerHandler: self.process_launch_info = process_launch_info self.process = None - self._received_shutdown = False + self._is_shutting_down = False self.request_id = 1 - self._response_handlers: dict[Any, Request] = {} + self._pending_requests: dict[Any, Request] = {} self.on_request_handlers = {} self.on_notification_handlers = {} self.logger = logger @@ -176,12 +202,12 @@ class SolidLanguageServerHandler: # start threads to read stdout and stderr of the process threading.Thread( - target=self.run_forever, + target=self._read_ls_process_stdout, name="LSP-stdout-reader", daemon=True, ).start() threading.Thread( - target=self.run_forever_stderr, + target=self._read_ls_process_stderr, name="LSP-stderr-reader", daemon=True, ).start() @@ -261,10 +287,10 @@ class SolidLanguageServerHandler: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit """ + self._is_shutting_down = True self._log("Sending shutdown request to server") self.send.shutdown() self._log("Received shutdown response from server") - self._received_shutdown = True self._log("Sending exit notification to server") self.notify.exit() self._log("Sent exit notification to server") @@ -279,27 +305,26 @@ class SolidLanguageServerHandler: @staticmethod def _read_bytes_from_process(process, stream, num_bytes): """Read exactly num_bytes from process stdout""" - if process.poll() is not None: - # Process has terminated, check if we can still read - pass - data = b"" while len(data) < num_bytes: chunk = stream.read(num_bytes - len(data)) if not chunk: if process.poll() is not None: - raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}") - # Process still running but no data available yet - time.sleep(0.01) # Small delay + raise LanguageServerTerminatedException( + f"Process terminated while trying to read response (read {num_bytes} of {len(data)} bytes before termination)" + ) + # Process still running but no data available yet, retry after a short delay + time.sleep(0.01) continue data += chunk return data - def run_forever(self) -> bool: + def _read_ls_process_stdout(self) -> None: """ Continuously read from the language server process stdout and handle the messages invoking the registered response and notification handlers """ + exception: Exception | None = None try: while self.process and self.process.stdout and self.process.stdout.readable(): line = self.process.stdout.readline() @@ -318,11 +343,20 @@ class SolidLanguageServerHandler: body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes) self._handle_body(body) - except (BrokenPipeError, ConnectionResetError): - pass - return self._received_shutdown + except LanguageServerTerminatedException as e: + exception = e + except (BrokenPipeError, ConnectionResetError) as e: + exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", cause=e) + except Exception as e: + exception = LanguageServerTerminatedException("Unexpected error while reading stdout from language server process", cause=e) + log.info("Language server stdout reader thread has terminated") + if not self._is_shutting_down: + if exception is None: + exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly") + log.error(str(exception)) + self._cancel_pending_requests(exception) - def run_forever_stderr(self) -> None: + def _read_ls_process_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages """ @@ -334,6 +368,7 @@ class SolidLanguageServerHandler: self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) except (BrokenPipeError, ConnectionResetError): pass + log.info("Language server stderr reader thread has terminated") def _handle_body(self, body: bytes) -> None: """ @@ -386,30 +421,40 @@ class SolidLanguageServerHandler: # Use lock to prevent race conditions on tasks and task_counter self._send_payload(make_error_response(request_id, err)) + def _cancel_pending_requests(self, exception: Exception) -> None: + """ + Cancel all pending requests by setting their results to an error + """ + with self._response_handlers_lock: + log.info("Cancelling %d pending language server requests", len(self._pending_requests)) + for request in self._pending_requests.values(): + log.info("Cancelling %s", request) + request.on_error(exception) + self._pending_requests.clear() + def send_request(self, method: str, params: dict | None = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ - request = Request() - - # Use lock to prevent race conditions on request_id and _response_handlers with self._request_id_lock: request_id = self.request_id self.request_id += 1 + request = Request(request_id=request_id, method=method) + log.debug("Starting: %s", request) + with self._response_handlers_lock: - self._response_handlers[request_id] = request + self._pending_requests[request_id] = request self._send_payload(make_request(method, request_id, params)) self._log(f"Waiting for response to request {method} with params:\n{params}") result = request.get_result(timeout=self._request_timeout) + log.debug("Completed: %s", request) self._log("Processing result") if result.is_error(): - raise LanguageServerException( - f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}" - ) from result.error + raise LanguageServerException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error self._log(f"Returning non-error result, which is:\n{result.payload}") return result.payload @@ -451,7 +496,7 @@ class SolidLanguageServerHandler: Handle the response received from the server for a request, using the id to determine the request """ with self._response_handlers_lock: - request = self._response_handlers.pop(response["id"]) + request = self._pending_requests.pop(response["id"]) if "result" in response and "error" not in response: request.on_result(response["result"]) @@ -499,7 +544,7 @@ class SolidLanguageServerHandler: except asyncio.CancelledError: return except Exception as ex: - if (not self._received_shutdown) and self.logger: + if (not self._is_shutting_down) and self.logger: self.logger( "client", "logger", diff --git a/src/solidlsp/ls_request.py b/src/solidlsp/ls_request.py index f81727d4..cacd2872 100644 --- a/src/solidlsp/ls_request.py +++ b/src/solidlsp/ls_request.py @@ -1,11 +1,13 @@ -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union -from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.lsp_protocol_handler import lsp_types +if TYPE_CHECKING: + from .ls_handler import SolidLanguageServerHandler + class LanguageServerRequest: - def __init__(self, handler: SolidLanguageServerHandler): + def __init__(self, handler: "SolidLanguageServerHandler"): self.handler = handler def _send_request(self, method: str, params: Any | None = None) -> Any: From 9368ce446cd41481f686569a41bed6ed49dffedd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 23:07:54 +0200 Subject: [PATCH 17/49] Rename Error -> LSPError --- src/solidlsp/ls.py | 4 ++-- src/solidlsp/ls_handler.py | 14 +++++++------- src/solidlsp/lsp_protocol_handler/server.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 7cb82d72..a2a42880 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -30,7 +30,7 @@ from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind from solidlsp.lsp_protocol_handler.server import ( - Error, + LSPError, ProcessLaunchInfo, StringDict, ) @@ -672,7 +672,7 @@ class SolidLanguageServer(ABC): response = self._send_references_request(relative_file_path, line=line, column=column) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - if isinstance(e, Error) and getattr(e, "code", None) == -32603: + if isinstance(e, LSPError) and getattr(e, "code", None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " "This often occurs when requesting references for a symbol not referenced in the expected way. " diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index ab8a6772..eef94cfe 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -20,7 +20,7 @@ from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes from solidlsp.lsp_protocol_handler.server import ( ENCODING, - Error, + LSPError, MessageType, PayloadLike, ProcessLaunchInfo, @@ -414,7 +414,7 @@ class SolidLanguageServerHandler: """ self._send_payload(make_response(request_id, params)) - def send_error_response(self, request_id: Any, err: Error) -> None: + def send_error_response(self, request_id: Any, err: LSPError) -> None: """ Send error response to the given request id to the server with the given error """ @@ -501,9 +501,9 @@ class SolidLanguageServerHandler: if "result" in response and "error" not in response: request.on_result(response["result"]) elif "result" not in response and "error" in response: - request.on_error(Error.from_lsp(response["error"])) + request.on_error(LSPError.from_lsp(response["error"])) else: - request.on_error(Error(ErrorCodes.InvalidRequest, "")) + request.on_error(LSPError(ErrorCodes.InvalidRequest, "")) def _request_handler(self, response: StringDict) -> None: """ @@ -516,7 +516,7 @@ class SolidLanguageServerHandler: if not handler: self.send_error_response( request_id, - Error( + LSPError( ErrorCodes.MethodNotFound, f"method '{method}' not handled on client.", ), @@ -524,10 +524,10 @@ class SolidLanguageServerHandler: return try: self.send_response(request_id, handler(params)) - except Error as ex: + except LSPError as ex: self.send_error_response(request_id, ex) except Exception as ex: - self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) + self.send_error_response(request_id, LSPError(ErrorCodes.InternalError, str(ex))) def _notification_handler(self, response: StringDict) -> None: """ diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py index 0cb59490..62d50e7f 100644 --- a/src/solidlsp/lsp_protocol_handler/server.py +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -59,7 +59,7 @@ class ProcessLaunchInfo: cwd: str = os.getcwd() -class Error(Exception): +class LSPError(Exception): def __init__(self, code: ErrorCodes, message: str) -> None: super().__init__(message) self.code = code @@ -68,8 +68,8 @@ class Error(Exception): return {"code": self.code, "message": super().__str__()} @classmethod - def from_lsp(cls, d: StringDict) -> "Error": - return Error(d["code"], d["message"]) + def from_lsp(cls, d: StringDict) -> "LSPError": + return LSPError(d["code"], d["message"]) def __str__(self) -> str: return f"{super().__str__()} ({self.code})" @@ -79,7 +79,7 @@ def make_response(request_id: Any, params: PayloadLike) -> StringDict: return {"jsonrpc": "2.0", "id": request_id, "result": params} -def make_error_response(request_id: Any, err: Error) -> StringDict: +def make_error_response(request_id: Any, err: LSPError) -> StringDict: return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()} From bb0e944b6ddd5342c20c481f9cffabce92a01e75 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 23:09:17 +0200 Subject: [PATCH 18/49] Rename LanguageServerException -> SolidLSPException (making clear that the exception originates from the proxy framework of SolidLSP) --- src/serena/tools/tools_base.py | 4 ++-- .../csharp_language_server.py | 22 +++++++++---------- src/solidlsp/language_servers/omnisharp.py | 4 ++-- src/solidlsp/ls.py | 20 ++++++++--------- src/solidlsp/ls_exceptions.py | 2 +- src/solidlsp/ls_handler.py | 4 ++-- src/solidlsp/ls_utils.py | 22 +++++++++---------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index c8163390..cd025970 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -15,7 +15,7 @@ from serena.prompt_factory import PromptFactory from serena.symbol import LanguageServerSymbolRetriever from serena.util.class_decorators import singleton from serena.util.inspection import iter_subclasses -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException if TYPE_CHECKING: from serena.agent import LinesRead, MemoriesManager, SerenaAgent @@ -239,7 +239,7 @@ class Tool(Component): # apply the actual tool try: result = apply_fn(**kwargs) - except LanguageServerException as e: + except SolidLSPException as e: if e.is_language_server_terminated(): log.error(f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ...") self.agent.reset_language_server() diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 7d11a1df..cd1b83ad 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -20,7 +20,7 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -240,7 +240,7 @@ class CSharpLanguageServer(SolidLanguageServer): elif system == "linux": return "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64" else: - raise LanguageServerException(f"Unsupported platform: {system} {machine}") + raise SolidLSPException(f"Unsupported platform: {system} {machine}") @staticmethod def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]: @@ -255,9 +255,9 @@ class CSharpLanguageServer(SolidLanguageServer): dotnet_runtime_dep = dep if not lang_server_dep: - raise LanguageServerException(f"No C# language server dependency found for platform {runtime_id}") + raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}") if not dotnet_runtime_dep: - raise LanguageServerException(f"No .NET runtime dependency found for platform {runtime_id}") + raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}") return lang_server_dep, dotnet_runtime_dep @@ -300,7 +300,7 @@ class CSharpLanguageServer(SolidLanguageServer): CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir) if not server_dll.exists(): - raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") + raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") # Make executable on Unix systems if platform.system().lower() != "windows": @@ -326,7 +326,7 @@ class CSharpLanguageServer(SolidLanguageServer): source_dir = possible_dir break else: - raise LanguageServerException(f"Could not find language server files in package. Searched in {package_path}") + raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}") # Copy files to cache directory server_dir.mkdir(parents=True, exist_ok=True) @@ -358,7 +358,7 @@ class CSharpLanguageServer(SolidLanguageServer): break if not package_base_address: - raise LanguageServerException("Could not find package base address in Azure NuGet feed") + raise SolidLSPException("Could not find package base address in Azure NuGet feed") # Construct the download URL for the specific package package_id_lower = package_name.lower() @@ -385,7 +385,7 @@ class CSharpLanguageServer(SolidLanguageServer): return package_extract_dir except Exception as e: - raise LanguageServerException( + raise SolidLSPException( f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}" ) from e @@ -448,7 +448,7 @@ class CSharpLanguageServer(SolidLanguageServer): return str(dotnet_exe) except Exception as e: - raise LanguageServerException(f"Failed to download .NET 9 runtime from {url}: {e}") from e + raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e def _get_initialize_params(self) -> InitializeParams: """ @@ -623,7 +623,7 @@ class CSharpLanguageServer(SolidLanguageServer): self.server.start() except Exception as e: self.logger.log(f"Failed to start language server process: {e}", logging.ERROR) - raise LanguageServerException(f"Failed to start C# language server: {e}") + raise SolidLSPException(f"Failed to start C# language server: {e}") # Send initialization initialize_params = self._get_initialize_params() @@ -633,7 +633,7 @@ class CSharpLanguageServer(SolidLanguageServer): init_response = self.server.send.initialize(initialize_params) self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG) except Exception as e: - raise LanguageServerException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e + raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e # Apply diagnostic capabilities self._force_pull_diagnostics(init_response) diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index eafaa310..492eea32 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -14,7 +14,7 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -68,7 +68,7 @@ class OmniSharp(SolidLanguageServer): slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: logger.log("No *.sln file found in repository", logging.ERROR) - raise LanguageServerException("No SLN file found in repository") + raise SolidLSPException("No SLN file found in repository") cmd = " ".join( [ diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index a2a42880..2a8bc1ef 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -21,7 +21,7 @@ from serena.text_utils import MatchedConsecutiveLines from serena.util.file_system import match_path from solidlsp import ls_types from solidlsp.ls_config import Language, LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils @@ -209,7 +209,7 @@ class SolidLanguageServer(ABC): else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) - raise LanguageServerException(f"Language {config.code_language} is not supported") + raise SolidLSPException(f"Language {config.code_language} is not supported") ls.set_request_timeout(timeout) return ls @@ -427,7 +427,7 @@ class SolidLanguageServer(ABC): "open_file called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -483,7 +483,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -529,7 +529,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -573,7 +573,7 @@ class SolidLanguageServer(ABC): "request_definition called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): # sending request to the language server and waiting for response @@ -665,7 +665,7 @@ class SolidLanguageServer(ABC): "request_references called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): try: @@ -724,7 +724,7 @@ class SolidLanguageServer(ABC): "request_text_document_diagnostics called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): response = self.server.send.text_document_diagnostic( @@ -1286,7 +1286,7 @@ class SolidLanguageServer(ABC): "request_referencing_symbols called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") # First, get all references to the symbol references = self.request_references(relative_file_path, line, column) @@ -1558,7 +1558,7 @@ class SolidLanguageServer(ABC): "request_defining_symbol called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") # Get the definition location(s) definitions = self.request_definition(relative_file_path, line, column) diff --git a/src/solidlsp/ls_exceptions.py b/src/solidlsp/ls_exceptions.py index a3bb0f11..7c03cfe5 100644 --- a/src/solidlsp/ls_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -3,7 +3,7 @@ This module contains the exceptions raised by the framework. """ -class LanguageServerException(Exception): +class SolidLSPException(Exception): def __init__(self, message: str, cause: Exception | None = None): """ Initializes the exception with the given message. diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index eef94cfe..4565d2c5 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -14,7 +14,7 @@ from typing import Any import psutil from sensai.util.string import ToStringMixin -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_request import LanguageServerRequest from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes @@ -454,7 +454,7 @@ class SolidLanguageServerHandler: self._log("Processing result") if result.is_error(): - raise LanguageServerException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error + raise SolidLSPException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error self._log(f"Returning non-error result, which is:\n{result.payload}") return result.payload diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index d1161880..6daa3e7f 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -14,7 +14,7 @@ from pathlib import Path, PurePath import requests -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_types import UnifiedSymbolInformation @@ -170,13 +170,13 @@ class FileUtils: """ if not os.path.exists(file_path): logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) - raise LanguageServerException(f"File read '{file_path}' failed: File does not exist.") + raise SolidLSPException(f"File read '{file_path}' failed: File does not exist.") try: with open(file_path, encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) - raise LanguageServerException("File read failed.") from None + raise SolidLSPException("File read failed.") from None @staticmethod def download_file(logger: LanguageServerLogger, url: str, target_path: str) -> None: @@ -187,12 +187,12 @@ class FileUtils: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR) - raise LanguageServerException("Error downloading file.") + raise SolidLSPException("Error downloading file.") with open(target_path, "wb") as f: shutil.copyfileobj(response.raw, f) except Exception as exc: logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR) - raise LanguageServerException("Error downloading file.") from None + raise SolidLSPException("Error downloading file.") from None @staticmethod def download_and_extract_archive(logger: LanguageServerLogger, url: str, target_path: str, archive_type: str) -> None: @@ -220,10 +220,10 @@ class FileUtils: shutil.copyfileobj(f_in, f_out) else: logger.log(f"Unknown archive type '{archive_type}' for extraction", logging.ERROR) - raise LanguageServerException(f"Unknown archive type '{archive_type}'") + raise SolidLSPException(f"Unknown archive type '{archive_type}'") except Exception as exc: logger.log(f"Error extracting archive '{tmp_file_name}' obtained from '{url}': {exc}", logging.ERROR) - raise LanguageServerException("Error extracting archive.") from exc + raise SolidLSPException("Error extracting archive.") from exc finally: for tmp_file_name in tmp_files: if os.path.exists(tmp_file_name): @@ -289,7 +289,7 @@ class PlatformUtils: platform_id += "-" + libc return PlatformId(platform_id) else: - raise LanguageServerException(f"Unknown platform: {system=}, {machine=}, {bitness=}") + raise SolidLSPException(f"Unknown platform: {system=}, {machine=}, {bitness=}") @staticmethod def _determine_windows_machine_type(): @@ -345,7 +345,7 @@ class PlatformUtils: available_version_cmd_output.append(version_cmd_output) if not available_version_cmd_output: - raise LanguageServerException("dotnet not found on the system") + raise SolidLSPException("dotnet not found on the system") # Check for supported versions in order of preference (latest first) for version_cmd_output in available_version_cmd_output: @@ -361,7 +361,7 @@ class PlatformUtils: return DotnetVersion.V4 # If no supported version found, raise exception with all available versions - raise LanguageServerException( + raise SolidLSPException( f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8" ) except (FileNotFoundError, subprocess.CalledProcessError): @@ -369,7 +369,7 @@ class PlatformUtils: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) return DotnetVersion.VMONO except (FileNotFoundError, subprocess.CalledProcessError): - raise LanguageServerException("dotnet or mono not found on the system") + raise SolidLSPException("dotnet or mono not found on the system") class SymbolUtils: From c08a9b6e5599f6d020844394e20f5d040dae88e5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 28 Jul 2025 17:15:27 +0200 Subject: [PATCH 19/49] Support list-based commands for RuntimeDependency installation (preferable on Windows where spaces in paths can cause problems) Apply list-based commands in TypeScriptLanguageServer * for launch command * for runtime dependency installation --- src/solidlsp/language_servers/common.py | 12 ++++++++++-- .../language_servers/typescript_language_server.py | 8 ++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 4ce8f3b4..8163d5b3 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import os +import platform import subprocess from collections.abc import Sequence from dataclasses import dataclass @@ -21,7 +22,7 @@ class RuntimeDependency: url: str | None = None archive_type: str | None = None binary_name: str | None = None - command: str | None = None + command: str | list[str] | None = None package_name: str | None = None package_version: str | None = None extract_path: str | None = None @@ -71,7 +72,7 @@ class RuntimeDependencyCollection: return results @staticmethod - def _run_command(command: str, cwd: str) -> None: + def _run_command(command: str | list[str], cwd: str) -> None: kwargs = {} if PlatformUtils.get_platform_id().is_windows(): kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore @@ -80,6 +81,13 @@ class RuntimeDependencyCollection: kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name log.info("Running command '%s' in '%s'", command, cwd) + + is_windows = platform.system() == "Windows" + if not isinstance(command, str) and not is_windows: + # Since we are using the shell, we need to convert the command list to a single string + # on Linux/macOS + command = " ".join(command) + completed_process = subprocess.run( command, shell=True, diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 7b700deb..ccb5e8c4 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -73,7 +73,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): @classmethod def _setup_runtime_dependencies( cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings - ) -> str: + ) -> list[str]: """ Setup runtime dependencies for TypeScript Language Server and return the command to start the server. """ @@ -95,13 +95,13 @@ class TypeScriptLanguageServer(SolidLanguageServer): RuntimeDependency( id="typescript", description="typescript package", - command="npm install --prefix ./ typescript@5.5.4", + command=["npm", "install", "--prefix", "./", "typescript@5.5.4"], platform_id="any", ), RuntimeDependency( id="typescript-language-server", description="typescript-language-server package", - command="npm install --prefix ./ typescript-language-server@4.3.3", + command=["npm", "install", "--prefix", "./", "typescript-language-server@4.3.3"], platform_id="any", ), ] @@ -131,7 +131,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): raise FileNotFoundError( f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation." ) - return f"{tsserver_executable_path} --stdio" + return [tsserver_executable_path, "--stdio"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: From 68fcc690609c235100b3c094f16490e6c9aacbc3 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 28 Jul 2025 18:01:53 +0200 Subject: [PATCH 20/49] Improve logging --- src/solidlsp/language_servers/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 8163d5b3..1587d0a9 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -80,7 +80,6 @@ class RuntimeDependencyCollection: import pwd kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name - log.info("Running command '%s' in '%s'", command, cwd) is_windows = platform.system() == "Windows" if not isinstance(command, str) and not is_windows: @@ -88,6 +87,8 @@ class RuntimeDependencyCollection: # on Linux/macOS command = " ".join(command) + log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd) + completed_process = subprocess.run( command, shell=True, From dcbcd5c6d035fc2999c0f074fbfc283ca0a3e698 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 00:12:10 +0200 Subject: [PATCH 21/49] Do not truncate messages when tracing ls communication --- src/solidlsp/ls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index e0a09f4d..ded2645a 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -281,7 +281,7 @@ class SolidLanguageServer(ABC): if config.trace_lsp_communication: def logging_fn(source: str, target: str, msg: StringDict | str): - self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) + self.logger.log(f"LSP: {source} -> {target}: {msg!s}", self.logger.logger.level) else: logging_fn = None From 1324f1a3f2be89e8886277c47569421426bf876f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 00:12:49 +0200 Subject: [PATCH 22/49] Log all messages written to the LS's stderr stream at ERROR level --- src/solidlsp/ls_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 4565d2c5..91574c1c 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -365,7 +365,7 @@ class SolidLanguageServerHandler: line = self.process.stderr.readline() if not line: continue - self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) + log.error(line.decode(ENCODING, errors="replace")) except (BrokenPipeError, ConnectionResetError): pass log.info("Language server stderr reader thread has terminated") From 72bfb611c273e6fce7d8a69f0647fb1e6e43bfdd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 00:35:29 +0200 Subject: [PATCH 23/49] Fix termination detection in stderr/stdout LS reader threads --- src/solidlsp/ls_handler.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 91574c1c..6a994ed3 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -134,7 +134,7 @@ class SolidLanguageServerHandler: self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info - self.process = None + self.process: subprocess.Popen | None = None self._is_shutting_down = False self.request_id = 1 @@ -326,7 +326,9 @@ class SolidLanguageServerHandler: """ exception: Exception | None = None try: - while self.process and self.process.stdout and self.process.stdout.readable(): + while self.process and self.process.stdout: + if self.process.poll() is not None: # process has terminated + break line = self.process.stdout.readline() if not line: continue @@ -361,14 +363,20 @@ class SolidLanguageServerHandler: Continuously read from the language server process stderr and log the messages """ try: - while self.process and self.process.stderr and self.process.stderr.readable(): + while self.process and self.process.stderr: + if self.process.poll() is not None: + # process has terminated + break line = self.process.stderr.readline() if not line: continue log.error(line.decode(ENCODING, errors="replace")) - except (BrokenPipeError, ConnectionResetError): - pass - log.info("Language server stderr reader thread has terminated") + except Exception as e: + log.error("Error while reading stderr from language server process: %s", e, exc_info=e) + if not self._is_shutting_down: + log.error("Language server stderr reader thread terminated unexpectedly") + else: + log.info("Language server stderr reader thread has terminated") def _handle_body(self, body: bytes) -> None: """ From 53b5bcb71bd1773f0a34f79cf31e22e95f3777e5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 00:54:48 +0200 Subject: [PATCH 24/49] Update sensai-utils to 1.5.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e79567c2..e6ee41f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", "flask>=3.0.0", - "sensai-utils>=1.4.0", + "sensai-utils>=1.5.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", "pyyaml>=6.0.2", diff --git a/uv.lock b/uv.lock index 52fe9264..569fa4a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1104,14 +1104,14 @@ wheels = [ [[package]] name = "sensai-utils" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317, upload-time = "2025-01-27T22:47:51.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/dd/faa2e2de71a03af3def212c70777e794dd54ad5ab87927bb5c29f85f24fc/sensai_utils-1.5.0.tar.gz", hash = "sha256:2ca709a0d5807caf1632d665a455c173987b25276ce61693021672e875f0f17b", size = 61677, upload-time = "2025-07-28T22:46:10.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214, upload-time = "2025-01-27T22:47:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d5/62a0e693230bace8e9a767d6d187a4d9421a7c6ee4b48551f8ff7bd1629a/sensai_utils-1.5.0-py3-none-any.whl", hash = "sha256:c23c51d23d353e7a9b77c70aa8ba7f2d0a8fe4e67ee5bc1ef41c69dba5e4befb", size = 68836, upload-time = "2025-07-28T22:46:08.929Z" }, ] [[package]] @@ -1189,7 +1189,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.3,<3" }, { name = "ruamel-yaml", specifier = ">=0.18.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.285" }, - { name = "sensai-utils", specifier = ">=1.4.0" }, + { name = "sensai-utils", specifier = ">=1.5.0" }, { name = "sqlalchemy", marker = "extra == 'agno'", specifier = ">=2.0.40" }, { name = "syrupy", marker = "extra == 'dev'", specifier = ">=4.9.1" }, { name = "tiktoken", specifier = ">=0.9.0" }, From 1a07352034cc9fe43faf71f182832d2a5adb671e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 00:59:16 +0200 Subject: [PATCH 25/49] Fix broken shell configuration imposed by Claude Code (sets COMSPEC to Git-Bash on Windows) --- src/serena/agent.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index fe6a9091..66394d62 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -181,6 +181,8 @@ class SerenaAgent: log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") + self._check_shell_settings() + # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), # limited by the Serena config, the context (which is fixed for the session) and JetBrains mode tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] @@ -225,6 +227,16 @@ class SerenaAgent: except Exception as e: log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e) + def _check_shell_settings(self) -> None: + # On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces), + # which causes all sorts of trouble, preventing language servers from being launched correctly. + # So we make sure that COMSPEC is unset if it has been set to bash specifically. + if platform.system() == "Windows": + comspec = os.environ.get("COMSPEC", "") + if "bash" in comspec: + os.environ["COMSPEC"] = "" # force use of default shell + log.info("Adjusting COMSPEC environment variable to use the default shell instead of '%s'", comspec) + def _ide_context_tool_inclusion_definitions(self, project_root_or_name: str | None) -> list[ToolInclusionDefinition]: """ In the IDE assistant context, the agent is assumed to work on a single project, and we thus From 39e67acc63bd87c93f36d16f3099408c43dbcf3b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 01:05:51 +0200 Subject: [PATCH 26/49] Update change log --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c1b190..dfbf52c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,15 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Language servers: + * Reliably detect language server termination and propagate the respective error all the way + back to the tool application, where an unexpected termination is handled by restarting the language server + and subsequently retrying the tool application. + Fixes: * Ignore `.git` in check for ignored paths and improve performance of `find_all_non_ignored_files` +* Fix language server startup issues on Windows when using Claude Code (which was due to + default shell reconfiguration imposed by Claude Code) # 0.1.3 From e37c2af571cededebc8547f5db3024d3b4ddf616 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 29 Jul 2025 01:51:44 +0200 Subject: [PATCH 27/49] Update deprecation message --- src/serena/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 7591e0ca..deb3b6e6 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -435,7 +435,7 @@ class ProjectCommands(AutoRegisteringGroup): @click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False) @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING") def index_deprecated(project: str, log_level: str = "WARNING") -> None: - click.echo("Deprecated! Use `project index` instead.") + click.echo("Deprecated! Use `serena project index` instead.") ProjectCommands._index_project(project, log_level) @staticmethod From 3e49c83d89e29c08fbd6710a3ce4df3e342f3ffd Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Tue, 29 Jul 2025 15:55:07 +0200 Subject: [PATCH 28/49] Made initial instructions tool optional --- src/serena/tools/workflow_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 6172cc33..38cc19be 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -5,7 +5,7 @@ Tools supporting the general workflow of the agent import json import platform -from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject +from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional class CheckOnboardingPerformedTool(Tool): @@ -122,7 +122,7 @@ class PrepareForNewConversationTool(Tool): return self.prompt_factory.create_prepare_for_new_conversation() -class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject): +class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional): """ Gets the initial instructions for the current project. Should only be used in settings where the system prompt cannot be set, From 534567f11e087beed8012d968114f9560b4b8beb Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Tue, 29 Jul 2025 15:57:40 +0200 Subject: [PATCH 29/49] Excluded more tools in ide-assistant mode Namely: - prepare_for_new_conversation - summarize_changes - get_current_config --- src/serena/resources/config/contexts/ide-assistant.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index d40cb860..2a6c9e2c 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -22,4 +22,7 @@ excluded_tools: - delete_lines - replace_lines - insert_at_line - - execute_shell_command \ No newline at end of file + - execute_shell_command + - prepare_for_new_conversation + - summarize_changes + - get_current_config From d624c1c0b9241a549f50e1c202860932dbfe040d Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Wed, 30 Jul 2025 12:07:30 +0200 Subject: [PATCH 30/49] Allow passing a list of dirs as prompts_dir, the tail of the list serving as fallbacks --- src/interprompt/multilang_prompt.py | 96 +++++++++++++++++++++++++---- src/interprompt/prompt_factory.py | 6 +- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/interprompt/multilang_prompt.py b/src/interprompt/multilang_prompt.py index 55777344..e7322efb 100644 --- a/src/interprompt/multilang_prompt.py +++ b/src/interprompt/multilang_prompt.py @@ -1,7 +1,7 @@ import logging import os from enum import Enum -from typing import Any, Generic, TypeVar +from typing import Any, Generic, Literal, TypeVar import yaml from sensai.util.string import ToStringMixin @@ -92,6 +92,9 @@ class _MultiLangContainer(Generic[T], ToStringMixin): raise KeyError(f"Item for language '{lang_code}' already registered for name '{self.name}'") self._lang2item[lang_code] = item + def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool: + return lang_code in self._lang2item + def get_item(self, lang: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> T: """ Gets the item for the given language. @@ -183,6 +186,9 @@ class MultiLangPromptTemplate(ParameterizedTemplateInterface): prompt_template = self.get_prompt_template(lang_code, fallback_mode) return prompt_template.render(**params) + def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool: + return self._prompts_container.has_item(lang_code) + class MultiLangPromptList(_MultiLangContainer[PromptList]): pass @@ -212,44 +218,96 @@ class MultiLangPromptCollection: The prompt names must be unique (for the same language) within the collection. """ - def __init__(self, prompts_dir: str, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> None: + def __init__(self, prompts_dir: str | list[str], fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> None: """ - :param prompts_dir: the directory containing the prompt templates and prompt lists + :param prompts_dir: the directory containing the prompt templates and prompt lists. + If a list is provided, will look for prompt templates in the dirs from left to right + (first one containing the desired template wins). :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language. May be reset after initialization. """ self._multi_lang_prompt_templates: dict[str, MultiLangPromptTemplate] = {} self._multi_lang_prompt_lists: dict[str, MultiLangPromptList] = {} - self._load_from_disc(prompts_dir) + if isinstance(prompts_dir, str): + prompts_dir = [prompts_dir] + + # Add prompts from multiple directories, prioritizing names from the left. + # If name collisions appear in the first directory, an error is raised (so the first directory should have no + # internal collisions, this helps in avoiding errors) + # For all following directories, on a collision the new value will be ignored. + # This also means that for the following directories, there is no error check on collisions internal to them. + # We assume that they are correct (i.e., they have no internal collisions). + first_prompts_dir, fallback_prompt_dirs = prompts_dir[0], prompts_dir[1:] + self._load_from_disc(first_prompts_dir, on_name_collision="raise") + for fallback_prompt_dir in fallback_prompt_dirs: + # already loaded prompts have priority + self._load_from_disc(fallback_prompt_dir, on_name_collision="skip") + self.fallback_mode = fallback_mode - def _add_prompt_template(self, name: str, template_str: str, lang_code: str = DEFAULT_LANG_CODE) -> None: + def _add_prompt_template( + self, + name: str, + template_str: str, + lang_code: str = DEFAULT_LANG_CODE, + on_name_collision: Literal["skip", "overwrite", "raise"] = "raise", + ) -> None: """ :param name: name of the prompt template :param template_str: the Jinja template string :param lang_code: the language code for which to add the prompt template. + :param on_name_collision: how to deal with name/lang_code collisions """ + allow_overwrite = False prompt_template = PromptTemplate(name, template_str) mlpt = self._multi_lang_prompt_templates.get(name) if mlpt is None: mlpt = MultiLangPromptTemplate(name) self._multi_lang_prompt_templates[name] = mlpt - mlpt.add_prompt_template(prompt_template, lang_code=lang_code) + if mlpt.has_item(lang_code): + if on_name_collision == "raise": + raise KeyError(f"Prompt '{name}' for {lang_code} already exists!") + if on_name_collision == "skip": + log.debug(f"Skipping prompt '{name}' since it already exists.") + return + elif on_name_collision == "overwrite": + allow_overwrite = True + mlpt.add_prompt_template(prompt_template, lang_code=lang_code, allow_overwrite=allow_overwrite) - def _add_prompt_list(self, name: str, prompt_list: list[str], lang_code: str = DEFAULT_LANG_CODE) -> None: + def _add_prompt_list( + self, + name: str, + prompt_list: list[str], + lang_code: str = DEFAULT_LANG_CODE, + on_name_collision: Literal["skip", "overwrite", "raise"] = "raise", + ) -> None: """ :param name: name of the prompt list :param prompt_list: a list of prompts :param lang_code: the language code for which to add the prompt list. + :param on_name_collision: how to deal with name/lang_code collisions """ + allow_overwrite = False multilang_prompt_list = self._multi_lang_prompt_lists.get(name) if multilang_prompt_list is None: multilang_prompt_list = MultiLangPromptList(name) self._multi_lang_prompt_lists[name] = multilang_prompt_list - multilang_prompt_list.add_item(PromptList(prompt_list), lang_code=lang_code) + if multilang_prompt_list.has_item(lang_code): + if on_name_collision == "raise": + raise KeyError(f"Prompt '{name}' for {lang_code} already exists!") + if on_name_collision == "skip": + log.debug(f"Skipping prompt '{name}' since it already exists.") + return + elif on_name_collision == "overwrite": + allow_overwrite = True + multilang_prompt_list.add_item(PromptList(prompt_list), lang_code=lang_code, allow_overwrite=allow_overwrite) - def _load_from_disc(self, prompts_dir: str) -> None: - """Loads all prompt templates and prompt lists from yaml files in the given directory.""" + def _load_from_disc(self, prompts_dir: str, on_name_collision: Literal["skip", "overwrite", "raise"] = "raise") -> None: + """Loads all prompt templates and prompt lists from yaml files in the given directory. + + :param prompts_dir: + :param on_name_collision: how to deal with name/lang_code collisions + """ for fn in os.listdir(prompts_dir): if not fn.endswith((".yml", ".yaml")): log.debug(f"Skipping non-YAML file: {fn}") @@ -266,9 +324,11 @@ class MultiLangPromptCollection: # add the data to the collection for prompt_name, prompt_template_or_list in prompts_data.items(): if isinstance(prompt_template_or_list, list): - self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code) + self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision) elif isinstance(prompt_template_or_list, str): - self._add_prompt_template(prompt_name, prompt_template_or_list, lang_code=lang_code) + self._add_prompt_template( + prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision + ) else: raise ValueError( f"Invalid prompt type for {prompt_name} in file {path} (should be str or list): {prompt_template_or_list}" @@ -306,6 +366,18 @@ class MultiLangPromptCollection: """The PromptList object for the given prompt name and language code.""" return self.get_multilang_prompt_list(prompt_name).get_item(lang_code) + def _has_prompt_list(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool: + multi_lang_prompt_list = self._multi_lang_prompt_lists.get(prompt_name) + if multi_lang_prompt_list is None: + return False + return multi_lang_prompt_list.has_item(lang_code) + + def _has_prompt_template(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool: + multi_lang_prompt_template = self._multi_lang_prompt_templates.get(prompt_name) + if multi_lang_prompt_template is None: + return False + return multi_lang_prompt_template.has_item(lang_code) + def render_prompt_template( self, prompt_name: str, diff --git a/src/interprompt/prompt_factory.py b/src/interprompt/prompt_factory.py index 3aa9da08..1769e7f2 100644 --- a/src/interprompt/prompt_factory.py +++ b/src/interprompt/prompt_factory.py @@ -10,9 +10,11 @@ log = logging.getLogger(__name__) class PromptFactoryBase: """Base class for auto-generated prompt factory classes.""" - def __init__(self, prompts_dir: str, lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION): + def __init__(self, prompts_dir: str | list[str], lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION): """ - :param prompts_dir: the directory containing the prompt templates and prompt lists + :param prompts_dir: the directory containing the prompt templates and prompt lists. + If a list is provided, will look for prompt templates in the dirs from left to right + (first one containing the desired template wins). :param lang_code: the language code to use for retrieving the prompt templates and prompt lists. Leave as `default` for single-language use cases. :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language. From caa2c545e4c8870421e47c87e1b698f4c6fbc5d3 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Wed, 30 Jul 2025 12:10:15 +0200 Subject: [PATCH 31/49] New feature: allow users to override prompts. Extended CLI for that --- scripts/gen_prompt_factory.py | 4 +- src/serena/cli.py | 91 ++++++++++++++++++++++++++++++++++- src/serena/constants.py | 3 +- src/serena/prompt_factory.py | 7 ++- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/scripts/gen_prompt_factory.py b/scripts/gen_prompt_factory.py index 4b1354fd..4877d4d1 100644 --- a/scripts/gen_prompt_factory.py +++ b/scripts/gen_prompt_factory.py @@ -7,14 +7,14 @@ from pathlib import Path from sensai.util import logging from interprompt import autogenerate_prompt_factory_module -from serena.constants import PROMPT_TEMPLATES_DIR, REPO_ROOT +from serena.constants import PROMPT_TEMPLATES_DIR_INTERNAL, REPO_ROOT log = logging.getLogger(__name__) def main(): autogenerate_prompt_factory_module( - prompts_dir=PROMPT_TEMPLATES_DIR, + prompts_dir=PROMPT_TEMPLATES_DIR_INTERNAL, target_module_path=str(Path(REPO_ROOT) / "src" / "serena" / "generated" / "generated_prompt_factory.py"), ) diff --git a/src/serena/cli.py b/src/serena/cli.py index deb3b6e6..6b46a8e8 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -1,3 +1,4 @@ +import glob import os import shutil import subprocess @@ -16,6 +17,8 @@ from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths from serena.constants import ( DEFAULT_CONTEXT, DEFAULT_MODES, + PROMPT_TEMPLATES_DIR_IN_USER_HOME, + PROMPT_TEMPLATES_DIR_INTERNAL, SERENA_LOG_FORMAT, SERENA_MANAGED_DIR_IN_HOME, SERENAS_OWN_CONTEXT_YAMLS_DIR, @@ -476,12 +479,98 @@ class ToolCommands(AutoRegisteringGroup): ToolRegistry().print_tool_overview() +class PromptCommands(AutoRegisteringGroup): + def __init__(self) -> None: + super().__init__(name="prompts", help="Commands related to Serena's prompts that are outside of contexts and modes.") + + @staticmethod + def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str: + os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True) + return os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, prompt_yaml_name) + + @staticmethod + @click.command("list", help="Lists yamls that are used for defining prompts.") + def list() -> None: + serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")] + for prompt_yaml_name in serena_prompt_yaml_names: + user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name) + if os.path.exists(user_prompt_yaml_path): + click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}") + else: + click.echo(prompt_yaml_name) + + @staticmethod + @click.command("create-override", help="Create an override of an internal prompts yaml for customizing Serena's prompts") + @click.argument("prompt_yaml_name") + def create_override(prompt_yaml_name: str) -> None: + """ + :param prompt_yaml_name: The yaml name of the prompt you want to override. Call the `list` command for discovering valid prompt yaml names. + :return: + """ + # for convenience, we can pass names without .yml + if not prompt_yaml_name.endswith(".yml"): + prompt_yaml_name = prompt_yaml_name + ".yml" + user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name) + if os.path.exists(user_prompt_yaml_path): + raise FileExistsError(f"{user_prompt_yaml_path} already exists.") + serena_prompt_yaml_path = os.path.join(PROMPT_TEMPLATES_DIR_INTERNAL, prompt_yaml_name) + shutil.copyfile(serena_prompt_yaml_path, user_prompt_yaml_path) + _open_in_editor(user_prompt_yaml_path) + + @staticmethod + @click.command("edit-override", help="Edit an existing prompt override file") + @click.argument("prompt_yaml_name") + def edit_override(prompt_yaml_name: str) -> None: + """ + :param prompt_yaml_name: The yaml name of the prompt override to edit. + :return: + """ + # for convenience, we can pass names without .yml + if not prompt_yaml_name.endswith(".yml"): + prompt_yaml_name = prompt_yaml_name + ".yml" + user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name) + if not os.path.exists(user_prompt_yaml_path): + click.echo(f"Override file '{prompt_yaml_name}' not found. Create it with: prompts create-override {prompt_yaml_name}") + return + _open_in_editor(user_prompt_yaml_path) + + @staticmethod + @click.command("list-overrides", help="List existing prompt override files") + def list_overrides() -> None: + os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True) + serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")] + override_files = glob.glob(os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, "*.yml")) + for file_path in override_files: + if os.path.basename(file_path) in serena_prompt_yaml_names: + click.echo(file_path) + + @staticmethod + @click.command("delete-override", help="Delete a prompt override file") + @click.argument("prompt_yaml_name") + def delete_override(prompt_yaml_name: str) -> None: + """ + + :param prompt_yaml_name: The yaml name of the prompt override to delete." + :return: + """ + # for convenience, we can pass names without .yml + if not prompt_yaml_name.endswith(".yml"): + prompt_yaml_name = prompt_yaml_name + ".yml" + user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name) + if not os.path.exists(user_prompt_yaml_path): + click.echo(f"Override file '{prompt_yaml_name}' not found.") + return + os.remove(user_prompt_yaml_path) + click.echo(f"Deleted override file '{prompt_yaml_name}'.") + + # Expose groups so we can reference them in pyproject.toml mode = ModeCommands() context = ContextCommands() project = ProjectCommands() config = SerenaConfigCommands() tools = ToolCommands() +prompts = PromptCommands() # Expose toplevel commands for the same reason top_level = TopLevelCommands() @@ -489,7 +578,7 @@ start_mcp_server = top_level.start_mcp_server index_project = project.index_deprecated # needed for the help script to work - register all subcommands to the top-level group -for subgroup in (mode, context, project, config, tools): +for subgroup in (mode, context, project, config, tools, prompts): top_level.add_command(subgroup) diff --git a/src/serena/constants.py b/src/serena/constants.py index 9b0dd43f..d25f5c45 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -10,7 +10,8 @@ SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir) # TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here. REPO_ROOT = str(_repo_root_path) -PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") +PROMPT_TEMPLATES_DIR_INTERNAL = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") +PROMPT_TEMPLATES_DIR_IN_USER_HOME = str(_serena_in_home_managed_dir / "prompt_templates") SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts") """The contexts that are shipped with the Serena package, i.e. the default contexts.""" USER_CONTEXT_YAMLS_DIR = str(_serena_in_home_managed_dir / "contexts") diff --git a/src/serena/prompt_factory.py b/src/serena/prompt_factory.py index f4f02ca7..40f51e7a 100644 --- a/src/serena/prompt_factory.py +++ b/src/serena/prompt_factory.py @@ -1,4 +1,6 @@ -from serena.constants import PROMPT_TEMPLATES_DIR +import os + +from serena.constants import PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL from serena.generated.generated_prompt_factory import PromptFactory @@ -8,4 +10,5 @@ class SerenaPromptFactory(PromptFactory): """ def __init__(self) -> None: - super().__init__(PROMPT_TEMPLATES_DIR) + os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True) + super().__init__(prompts_dir=[PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL]) From d26dc3536af36d90c2156630326ce73ee89cbe96 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 31 Jul 2025 11:52:51 +0200 Subject: [PATCH 32/49] Readme: more info on CLI, initial instructions, and included_optional_tools --- README.md | 109 ++++++++++++++++++++++++++---------------------------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 24abe333..cfe3aa44 100644 --- a/README.md +++ b/README.md @@ -202,38 +202,27 @@ This and other settings can be adjusted in the [configuration](#configuration) a git clone https://github.com/oraios/serena cd serena ``` -2. Optionally create the configuration file in your home directory, i.e. - - * `~/.serena/serena_config.yml` on Linux and macOS, or - * `%USERPROFILE%\.serena\serena_config.yml` on Windows. - - by copying the template and then adjusting it according to your needs: +2. Optionally edit the configuration file in your home directory with ```shell - mkdir ~/.serena - cp src/serena/resources/serena_config.template.yml ~/.serena/serena_config.yml + uv run serena config edit ``` If you just want the default config, you can skip this part, and a config file will be created when you first run Serena. 3. Run the server with `uv`: ```shell - uv run serena-mcp-server + uv run serena start-mcp-server ``` - When running from outside the serena installation directory, be sure to pass it, i.e. use + When running from outside the serena installation directory, be sure to pass it, i.e., use ```shell - uv run --directory /abs/path/to/serena serena-mcp-server + uv run --directory /abs/path/to/serena serena start-mcp-server ``` ##### Using uvx `uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. -* Windows: - ```shell - uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe - ``` -* Other operating systems: - ```shell - uvx --from git+https://github.com/oraios/serena serena-mcp-server - ``` +```shell +uvx --from git+https://github.com/oraios/serena serena start-mcp-server +``` ##### Using Docker (Experimental) @@ -243,7 +232,7 @@ You can run the Serena MCP server directly via docker as follows, assuming that the projects you want to work on are all located in `/path/to/your/projects`: ```shell -docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena-mcp-server --transport stdio +docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena start-mcp-server --transport stdio ``` Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides: @@ -263,12 +252,12 @@ therefore needs to be configured with a launch command. When using instead the SSE mode, which uses HTTP-based communication, you control the server lifecycle yourself, i.e. you start the server and provide the client with the URL to connect to it. -Simply provide `serena-mcp-server` with the `--transport sse` option and optionally provide the port. +Simply provide `start-mcp-server` with the `--transport sse` option and optionally provide the port. For example, to run the Serena MCP server in SSE mode on port 9121 using a local installation, you would run this command from the Serena directory, ```shell -uv run serena-mcp-server --transport sse --port 9121 +uv run serena start-mcp-server --transport sse --port 9121 ``` and then configure your client to connect to `http://localhost:9121/sse`. @@ -289,12 +278,22 @@ Serena's behavior (active tools and prompts as well as logging configuration, et 1. The `serena_config.yml` for general settings that apply to all clients and projects. It is located in your user directory under `.serena/serena_config.yml`. If you do not explicitly create the file, it will be auto-generated when you first run Serena. -2. In the arguments passed to the `serena-mcp-server` in your client's config (see below), + You can edit it directly or use + ```shell + uvx --from git+https://github.com/oraios/serena serena config edit + ``` + (or use the `--directory` command version). +2. In the arguments passed to the `start-mcp-server` in your client's config (see below), which will apply to all sessions started by the respective client. In particular, the [context](#contexts) parameter should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client. See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments. 3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever - that project is activated. + that project is activated. This file will be autogenerated when you first use Serena on that project, but you can also + generate it explicitly with + ```shell + uvx --from git+https://github.com/oraios/serena serena project generate-yml + ``` + (or use the `--directory` command version). 4. Through the currently active set of [modes](#modes). @@ -323,20 +322,17 @@ project, the file `.serena/project.yml` will be generated. You can adjust the la same name. If you are mostly working with the same project, you can also configure to always activate a project at startup -by passing `--project ` to the `serena-mcp-server` command in your client's MCP config. +by passing `--project ` to the `start-mcp-server` command in your client's MCP config. ℹ️ For larger projects, we recommend that you index your project to accelerate Serena's tools; otherwise the first tool application may be very slow. -To do so, run one of these commands the project directory or pass the path to the project as an argument: +To do so, run this from the project directory (or pass the path to the project as an argument): -* When using a local installation: - ```shell - uv run --directory /abs/path/to/serena index-project - ``` -* When using uvx: - ```shell - uvx --from git+https://github.com/oraios/serena index-project - ``` +```shell +uvx --from git+https://github.com/oraios/serena serena project index +``` + +(or use the `--directory` command version). ### Claude Code @@ -351,18 +347,16 @@ claude mcp add serena -- --context ide-assistant --project $ where `` is your way of [running the Serena MCP server](#running-the-serena-mcp-server). For example, when using `uvx`, you would run ```shell -claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) +claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context ide-assistant --project $(pwd) ``` ℹ️ Serena comes with an instruction text, and Claude needs to read it to properly use Serena's tools. - As of version `v1.0.52`, claude code reads the instructions of the MCP server, so this is handled automatically. + As of version `v1.0.52`, claude code reads the instructions of the MCP server, so this **is handled automatically**. If you are using an older version, or if Claude fails to read the instructions, you can ask it explicitly - to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text. - You may have to do that whenever you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools. - -ℹ️ If Claude is not automatically picking up on the instructions and running Serena's tools, and you don't want to run the command in each session, - an alternative to the above is adding the instructions as part of the system prompt, then you will not need to run the command above or to remember re-running it after compacting. - This can be achieved through starting claude code with `claude --append-system-prompt $(uvx --from git+https://github.com/oraios/serena serena print-system-prompt)`. Note that this is **experimental**, Claude may not understand the instructions correctly in this way, and we haven't thoroughly tested the resulting behavior. Please report any issues you encounter. + to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text. + If you want to make use of that, you will have to enable the corresponding tool explicitly by adding `initial_instructions` to the `included_optional_tools` + in your config. + Note that you may have to make Claude read the instructions you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools. ### Claude Desktop @@ -377,7 +371,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se "mcpServers": { "serena": { "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] + "args": ["run", "--directory", "/abs/path/to/serena", "serena", "start-mcp-server"] } } } @@ -388,7 +382,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se "mcpServers": { "serena": { "command": "/abs/path/to/uvx", - "args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"] + "args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"] } } } @@ -399,7 +393,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se "mcpServers": { "serena": { "command": "docker", - "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-mcp-server", "--transport", "stdio"] + "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena", "start-mcp-server", "--transport", "stdio"] } } } @@ -581,14 +575,20 @@ When launching Serena, specify modes using `--mode `; multiple modes #### Customization You can create your own contexts and modes to precisely tailor Serena to your needs in two ways: -* **Adding to Serena's configuration directory**: Create new `.yml` files in the `config/contexts/` or `config/modes/` directories within your local Serena repository. These custom contexts/modes will be automatically registered and available for use by their name (filename without the `.yml` extension). They will also appear in listings of available contexts/modes. -* **Using external YAML files**: When starting Serena, you can provide an absolute path to a custom `.yml` file for a context or mode. +* You can use Serena's CLI to manage modes and contexts. Check out + ```shell + uvx --from git+https://github.com/oraios/serena serena mode --help + uvx --from git+https://github.com/oraios/serena serena context --help + ``` + *NOTE*: Custom contexts/modes are simply YAML files in `/.serena`, they are automatically registered and available for use by their name (filename without the `.yml` extension). If you don't want to use Serena's CLI, you can create and manage them in any way you see fit. +* **Using external YAML files**: When starting Serena, you can also provide an absolute path to a custom `.yml` file for a context or mode. A context or mode YAML file typically defines: * `name`: (Optional if filename is used) The name of the context/mode. * `prompt`: A string that will be incorporated into Serena's system prompt. -* `description`: (Optional) A brief description. +* `description`: (Optional) A brief description, not passed to the LLM. * `excluded_tools`: A list of tool names (strings) to disable when this context/mode is active. +* `included_optional_tools`: A list of tool names that are disabled by default and have to be explicitly enabled by the user. This customization allows for deep integration and adaptation of Serena to specific project requirements or personal preferences. @@ -741,11 +741,10 @@ subscriptions. ### Subscription-Based Coding Agents -The most prominent subscription-based coding agents are parts of IDEs like +Many prominent subscription-based coding agents are parts of IDEs like Windsurf, Cursor and VSCode. Serena's functionality is similar to Cursor's Agent, Windsurf's Cascade or -VSCode's -upcoming [agent mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode). +VSCode's agent mode. Serena has the advantage of not requiring a subscription. A potential disadvantage is that it @@ -753,7 +752,7 @@ is not directly integrated into an IDE, so the inspection of newly written code is not as seamless. More technical differences are: -* Serena is not bound to a specific IDE. +* Serena is not bound to a specific IDE or CLI. Serena's MCP server can be used with any MCP client (including some IDEs), and the Agno-based agent provides additional ways of applying its functionality. * Serena is not bound to a specific large language model or API. @@ -829,7 +828,7 @@ For details on contributing, see [here](/CONTRIBUTING.md). ## Full List of Tools -Here is the full list of Serena's tools with a short description (output of `uv run serena-list-tools`): +Here is the full list of Serena's tools with a short description (output of `uv run serena tools list`): * `activate_project`: Activates a project by name. * `check_onboarding_performed`: Checks whether project onboarding was already performed. @@ -843,9 +842,7 @@ Here is the full list of Serena's tools with a short description (output of `uv * `get_active_project`: Gets the name of the currently active project (if any) and lists existing projects * `get_current_config`: Prints the current configuration of the agent, including the active modes, tools, and context. * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. - * `initial_instructions`: Gets the initial instructions for the current project. - Should only be used in settings where the system prompt cannot be set, - e.g. in clients you have no control over, like Claude Desktop. + * `initial_instructions`: Gets the initial instructions for the current project. Disabled by default, has to be enabled explicitly by using `included_optional_tools` * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. * `insert_at_line`: Inserts content at a given line in a file. * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. From c69e30a7bc50338bd4a3af936a3f788969e1b573 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 31 Jul 2025 11:57:11 +0200 Subject: [PATCH 33/49] Readme: moved uvx install option above the local installation option [ci skip] --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cfe3aa44..1b6777cb 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,16 @@ Note that no matter how you run the MCP server, Serena will, by default, start a MCP server (since many clients fail to clean up processes correctly). This and other settings can be adjusted in the [configuration](#configuration) and/or by providing [command-line arguments](#command-line-arguments). +##### Using uvx + +`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. + +```shell +uvx --from git+https://github.com/oraios/serena serena start-mcp-server +``` + +Explore the CLI to see some of the customization options that serena provides (more info on them below). + ###### Local Installation 1. Clone the repository and change into it. @@ -214,15 +224,7 @@ This and other settings can be adjusted in the [configuration](#configuration) a When running from outside the serena installation directory, be sure to pass it, i.e., use ```shell uv run --directory /abs/path/to/serena serena start-mcp-server - ``` - -##### Using uvx - -`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. - -```shell -uvx --from git+https://github.com/oraios/serena serena start-mcp-server -``` + ``` ##### Using Docker (Experimental) From 9cf5ee22a6ca8e9d2b742cc6bf5e56ab8b6b37a2 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 31 Jul 2025 23:10:32 +0200 Subject: [PATCH 34/49] Hotfix: pin mcp dependency to 1.12.2 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6ee41f3..1c2dfced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "pyright>=1.1.396,<2", "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", - "mcp>=1.5.0", + "mcp==1.12.2", "flask>=3.0.0", "sensai-utils>=1.5.0", "pydantic>=2.10.6", diff --git a/uv.lock b/uv.lock index b4ab8659..1f278a04 100644 --- a/uv.lock +++ b/uv.lock @@ -1174,7 +1174,7 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, { name = "joblib", specifier = ">=1.5.1" }, - { name = "mcp", specifier = ">=1.5.0" }, + { name = "mcp", specifier = "==1.12.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.1" }, { name = "overrides", specifier = ">=7.7.0,<8" }, { name = "pathspec", specifier = ">=0.12.1" }, From 9af709eb7b8d7221c3dfd782593fb609dec55202 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 31 Jul 2025 23:35:08 +0200 Subject: [PATCH 35/49] MCP: update to 1.12.3, pass kwargs without dumping settings The code also works for 1.12.2, but the new version has typed params in the constructor, and is thus better. --- pyproject.toml | 2 +- src/serena/mcp.py | 4 +--- uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c2dfced..8872384c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "pyright>=1.1.396,<2", "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", - "mcp==1.12.2", + "mcp==1.12.3", "flask>=3.0.0", "sensai-utils>=1.5.0", "pydantic>=2.10.6", diff --git a/src/serena/mcp.py b/src/serena/mcp.py index f5f785e0..9926af8e 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -170,10 +170,8 @@ class SerenaMCPFactory: # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; # retain only FASTMCP_ prefix for already set environment variables. Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") - - mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port) instructions = self._get_initial_instructions() - mcp = FastMCP(**mcp_settings.model_dump(), instructions=instructions) + mcp = FastMCP(lifespan=self.server_lifespan, host=host, port=port, instructions=instructions) return mcp @asynccontextmanager diff --git a/uv.lock b/uv.lock index 1f278a04..a1169097 100644 --- a/uv.lock +++ b/uv.lock @@ -558,7 +558,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.12.2" +version = "1.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -573,9 +573,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/85/f36d538b1286b7758f35c1b69d93f2719d2df90c01bd074eadd35f6afc35/mcp-1.12.2.tar.gz", hash = "sha256:a4b7c742c50ce6ed6d6a6c096cca0e3893f5aecc89a59ed06d47c4e6ba41edcc", size = 426202, upload-time = "2025-07-24T18:29:05.175Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203, upload-time = "2025-07-31T18:36:36.795Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/cf/3fd38cfe43962452e4bfadc6966b2ea0afaf8e0286cb3991c247c8c33ebd/mcp-1.12.2-py3-none-any.whl", hash = "sha256:b86d584bb60193a42bd78aef01882c5c42d614e416cbf0480149839377ab5a5f", size = 158473, upload-time = "2025-07-24T18:29:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810, upload-time = "2025-07-31T18:36:34.915Z" }, ] [[package]] @@ -1174,7 +1174,7 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, { name = "joblib", specifier = ">=1.5.1" }, - { name = "mcp", specifier = "==1.12.2" }, + { name = "mcp", specifier = "==1.12.3" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.1" }, { name = "overrides", specifier = ">=7.7.0,<8" }, { name = "pathspec", specifier = ">=0.12.1" }, From 88ceb4920031c1b059c783e6292f442699be1c63 Mon Sep 17 00:00:00 2001 From: Jason Stillwell Date: Sat, 2 Aug 2025 02:30:34 -0500 Subject: [PATCH 36/49] Don't subvert the gem system and try to run the binary directly. Instead use the framework provided by ruby gems to execute the provide binary. --- src/solidlsp/language_servers/solargraph.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 56a1b7f7..7b79c324 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -87,19 +87,7 @@ class Solargraph(SolidLanguageServer): logger.log("Installing Solargraph...", logging.INFO) subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) - # Get the gem executable path directly - result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) - gem_path = result.stdout.strip() - bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") - executable_path = os.path.join(bin_dir, "solargraph") - - if not os.path.exists(executable_path): - raise RuntimeError(f"Solargraph executable not found at {executable_path}") - - # Ensure the executable has the right permissions - os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) - - return executable_path + return "gem exec solargraph" except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e From c44a6be58f4a4447a9d057b3ca98331edb2297c5 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 2 Aug 2025 17:38:20 +0200 Subject: [PATCH 37/49] Docs on connecting Serena to chatgpt --- docs/serena_on_chatgpt.md | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/serena_on_chatgpt.md diff --git a/docs/serena_on_chatgpt.md b/docs/serena_on_chatgpt.md new file mode 100644 index 00000000..64f7e248 --- /dev/null +++ b/docs/serena_on_chatgpt.md @@ -0,0 +1,101 @@ + +# Connecting Serena MCP Server to ChatGPT via MCPO & Cloudflare Tunnel + +This guide explains how to expose a **locally running Serena MCP server** (powered by MCPO) to the internet using **Cloudflare Tunnel**, and how to connect it to **ChatGPT as a Custom GPT with tool access**. + +Once configured, ChatGPT becomes a powerful **coding agent** with direct access to your codebase, shell, and file system — so **read the security notes carefully**. + +--- +## Prerequisites + +Make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/) +and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed. + +## 1. Start the Serena MCP Server via MCPO + +Run the following command to launch Serena as http server (assuming port 8000): + +```bash +uvx mcpo --port 8000 --api-key -- \ + uvx --from git+https://github.com/oraios/serena \ + serena start-mcp-server --project $(pwd) +``` + +- `--api-key` is required to secure the server. +- `--project` should point to the root of your codebase. + +You can also use other options, and you don't have to pass `--project` if you want to work on multiple projects +or want to activate it later. See + +```shell +uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help +``` + +--- + +## 2. Expose the Server Using Cloudflare Tunnel + +Run: + +```bash +cloudflared tunnel --url http://localhost:8000 +``` + +This will give you a **public HTTPS URL** like: + +``` +https://serena-agent-tunnel.trycloudflare.com +``` + +Your server is now securely exposed to the internet. + +--- + +## 3. Connect It to ChatGPT (Custom GPT) + +### Steps: + +1. Go to [ChatGPT → Explore GPTs → Create](https://chat.openai.com/gpts/editor) +2. During setup, click **“Add APIs”** +3. Set up **API Key authentication** with the auth type as **Bearer** and enter the api key you used to start the MCPO server. +4. In the **Schema** section, click on **import from URL** and paste `/openapi.json` with the URL you got from the previous step. +5. Add the following line to the top of the imported JSON schema: + ``` + "servers": ["url": ""], + ``` + +ChatGPT will read the schema and create functions automatically. + +--- + +## Security Warning — Read Carefully + +Depending on your configuration and enabled tools, Serena's MCP server may: +- Execute **arbitrary shell commands** +- Read, write, and modify **files in your codebase** + +This gives ChatGPT the same powers as a remote developer on your machine. + +### ⚠️ Key Rules: +- **NEVER expose your API key** +- **Only expose this server when needed**, and monitor its use. + +In your project’s `.serena/project.yml` or global config, you can disable tools like: + +```yaml +excluded_tools: + - execute_shell_command + - ... +read_only: true +``` + +This is strongly recommended if you want a read-only or safer agent. + + +--- + +## Final Thoughts + +With this setup, ChatGPT becomes a coding assistant **running on your local code** — able to index, search, edit, and even run shell commands depending on your configuration. + +Use responsibly, and keep security in mind. From e41d09d9372d580e92835ca4980c00a432a9b03d Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 2 Aug 2025 18:55:34 +0200 Subject: [PATCH 38/49] Formatting --- src/solidlsp/language_servers/solargraph.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 7b79c324..a4f7e694 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -7,7 +7,6 @@ import json import logging import os import pathlib -import stat import subprocess import threading From 46967fa3cbe5af325bcc9c07722ee9ee7d42aff9 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 2 Aug 2025 18:56:09 +0200 Subject: [PATCH 39/49] Allow overriding tool descriptions in the context, cli command for tool description --- src/serena/agent.py | 10 ++++++++ src/serena/cli.py | 24 ++++++++++++++++++- src/serena/config/context_mode.py | 12 ++++++++-- src/serena/mcp.py | 11 +++++++-- .../resources/config/contexts/agent.yml | 4 +++- .../config/contexts/context.template.yml | 6 ++++- .../resources/config/contexts/desktop-app.yml | 3 ++- .../config/contexts/ide-assistant.yml | 3 +++ 8 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 66394d62..7e900f34 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -227,6 +227,12 @@ class SerenaAgent: except Exception as e: log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e) + def get_context(self) -> SerenaAgentContext: + return self._context + + def get_tool_description_override(self, tool_name: str) -> str | None: + return self._context.tool_description_overrides.get(tool_name, None) + def _check_shell_settings(self) -> None: # On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces), # which causes all sorts of trouble, preventing language servers from being launched correctly. @@ -583,3 +589,7 @@ class SerenaAgent: if self._gui_log_viewer: log.info("Stopping the GUI log window ...") self._gui_log_viewer.stop() + + def get_tool_by_name(self, tool_name: str) -> Tool: + tool_class = ToolRegistry().get_tool_class_by_name(tool_name) + return self.get_tool(tool_class) diff --git a/src/serena/cli.py b/src/serena/cli.py index 6b46a8e8..457b3acb 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -26,7 +26,7 @@ from serena.constants import ( USER_CONTEXT_YAMLS_DIR, USER_MODE_YAMLS_DIR, ) -from serena.mcp import SerenaMCPFactorySingleProcess +from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess from serena.project import Project from serena.tools import ToolRegistry from serena.util.logging import MemoryLogHandler @@ -478,6 +478,28 @@ class ToolCommands(AutoRegisteringGroup): else: ToolRegistry().print_tool_overview() + @staticmethod + @click.command( + "description", + help="Print the description of a tool, optionally with a specific context (the latter may modify the default description).", + ) + @click.argument("tool_name", type=str) + @click.option("--context", type=str, default=None, help="Context name or path to context file.") + def description(tool_name: str, context: str | None = None) -> None: + # Load the context + serena_context = None + if context: + serena_context = SerenaAgentContext.load(context) + + agent = SerenaAgent( + project=None, + serena_config=SerenaConfig(web_dashboard=False, log_level=logging.INFO), + context=serena_context, + ) + tool = agent.get_tool_by_name(tool_name) + mcp_tool = SerenaMCPFactory.make_mcp_tool(tool) + click.echo(mcp_tool.description) + class PromptCommands(AutoRegisteringGroup): def __init__(self) -> None: diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index de84f6a7..30caba0b 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -4,7 +4,7 @@ Context and Mode configuration loader import os from copy import copy -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, field from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self @@ -134,6 +134,8 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): name: str prompt: str description: str = "" + tool_description_overrides: dict[str, str] = field(default_factory=dict) + """Maps tool names to custom descriptions, default descriptions are extracted from the tool docstrings.""" def _tostring_includes(self) -> list[str]: return ["name"] @@ -147,6 +149,9 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): def from_json_dict(cls, data: dict) -> Self: data = copy(data) data["excluded_tools"] = set(data["excluded_tools"]) + # Ensure backwards compatibility for tool_description_overrides + if "tool_description_overrides" not in data: + data["tool_description_overrides"] = {} return cls(**data) @classmethod @@ -155,7 +160,10 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): with open(yaml_path, encoding="utf-8") as f: data = yaml.safe_load(f) name = data.pop("name", Path(yaml_path).stem) - return cls(name=name, **data) + prompt = data["prompt"] + description = data.get("description", "") + tool_description_overrides = data.get("tool_description_overrides", {}) + return cls(name=name, prompt=prompt, description=description, tool_description_overrides=tool_description_overrides) @classmethod def get_path(cls, name: str) -> str: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 9926af8e..6c75556f 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -68,10 +68,16 @@ class SerenaMCPFactory: # Mount the tool description as a combination of the docstring description and # the return value description, if it exists. - if docstring.description: - func_doc = f"{docstring.description.strip().strip('.')}." + agent = cast(SerenaAgent, tool.agent) + overridden_description = agent.get_context().tool_description_overrides.get(func_name, None) + + if overridden_description is not None: + func_doc = overridden_description + elif docstring.description: + func_doc = docstring.description else: func_doc = "" + func_doc = func_doc.strip().strip(".") if docstring.returns and (docstring_returns_descr := docstring.returns.description): # Only add a space before "Returns" if func_doc is not empty prefix = " " if func_doc else "" @@ -113,6 +119,7 @@ class SerenaMCPFactory: for tool in self._iter_tools(): mcp_tool = self.make_mcp_tool(tool) mcp._tool_manager._tools[tool.get_name()] = mcp_tool + log.info(f"Starting MCP server with {len(mcp._tool_manager._tools)} tools: {list(mcp._tool_manager._tools.keys())}") @abstractmethod def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: diff --git a/src/serena/resources/config/contexts/agent.yml b/src/serena/resources/config/contexts/agent.yml index d3da9da9..e2984283 100644 --- a/src/serena/resources/config/contexts/agent.yml +++ b/src/serena/resources/config/contexts/agent.yml @@ -3,4 +3,6 @@ prompt: | You are running in agent context where the system prompt is provided externally. You should use symbolic tools when possible for code understanding and modification. excluded_tools: - - initial_instructions \ No newline at end of file + - initial_instructions + +tool_description_overrides: {} \ No newline at end of file diff --git a/src/serena/resources/config/contexts/context.template.yml b/src/serena/resources/config/contexts/context.template.yml index 52e94535..f9dee588 100644 --- a/src/serena/resources/config/contexts/context.template.yml +++ b/src/serena/resources/config/contexts/context.template.yml @@ -1,4 +1,8 @@ # See Serena's documentation for more details on concept of contexts. description: Description of the context, not used in the code. prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context. -excluded_tools: [] \ No newline at end of file +excluded_tools: [] + +# mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method). +# Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason. +tool_description_overrides: {} \ No newline at end of file diff --git a/src/serena/resources/config/contexts/desktop-app.yml b/src/serena/resources/config/contexts/desktop-app.yml index ea1567fb..8105ca8d 100644 --- a/src/serena/resources/config/contexts/desktop-app.yml +++ b/src/serena/resources/config/contexts/desktop-app.yml @@ -10,4 +10,5 @@ prompt: | If complex relationships that the user asked for should be visualized or explained, consider creating a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions. -excluded_tools: [] \ No newline at end of file +excluded_tools: [] +tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index 2a6c9e2c..14d65130 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -26,3 +26,6 @@ excluded_tools: - prepare_for_new_conversation - summarize_changes - get_current_config + +tool_description_overrides: {} + From e9e05c2f3fca4a72b10900359c2644f138b412bc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 2 Aug 2025 20:32:34 +0200 Subject: [PATCH 40/49] Allow system prompt to depend on available tools and tool categories (as indicated by ToolMarkers) * Introduce additional tool markers that represent relevant categories * Apply some conditions to current system prompt --- src/serena/agent.py | 18 ++++++++++-- .../generated/generated_prompt_factory.py | 4 ++- .../config/prompt_templates/system_prompt.yml | 29 ++++++++++++------- src/serena/tools/jetbrains_tools.py | 8 ++--- src/serena/tools/symbol_tools.py | 20 ++++++++----- src/serena/tools/tools_base.py | 24 +++++++++++++-- src/solidlsp/language_servers/solargraph.py | 1 - 7 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 66394d62..5f27a1d1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -25,7 +25,8 @@ from serena.config.serena_config import SerenaConfig, ToolInclusionDefinition, T from serena.dashboard import SerenaDashboardAPI from serena.project import Project from serena.prompt_factory import SerenaPromptFactory -from serena.tools import ActivateProjectTool, Tool, ToolRegistry +from serena.tools import ActivateProjectTool, Tool, ToolMarker, ToolRegistry +from serena.util.inspection import iter_subclasses from serena.util.logging import MemoryLogHandler from solidlsp import SolidLanguageServer @@ -336,10 +337,23 @@ class SerenaAgent: return list(self._modes) def create_system_prompt(self) -> str: - return self.prompt_factory.create_system_prompt( + available_tools = [tool.get_name_from_cls() for tool in self.get_exposed_tool_instances()] + + # determine available tool markers (i.e. tool categories) + available_markers = set() + for marker_class in iter_subclasses(ToolMarker): + for tool in self.get_exposed_tool_instances(): + if isinstance(tool, marker_class): + available_markers.add(marker_class.__name__) + + log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers) + system_prompt = self.prompt_factory.create_system_prompt( context_system_prompt=self._context.prompt, mode_system_prompts=[mode.prompt for mode in self._modes], + available_tools=available_tools, + available_markers=available_markers, ) + log.info("System prompt:\n%s", system_prompt) def _update_active_tools(self) -> None: """ diff --git a/src/serena/generated/generated_prompt_factory.py b/src/serena/generated/generated_prompt_factory.py index 6bb7a76b..3a5e1442 100644 --- a/src/serena/generated/generated_prompt_factory.py +++ b/src/serena/generated/generated_prompt_factory.py @@ -32,5 +32,7 @@ class PromptFactory(PromptFactoryBase): def create_prepare_for_new_conversation(self) -> str: return self._render_prompt("prepare_for_new_conversation", locals()) - def create_system_prompt(self, *, context_system_prompt: Any, mode_system_prompts: Any) -> str: + def create_system_prompt( + self, *, available_markers: Any, available_tools: Any, context_system_prompt: Any, mode_system_prompts: Any + ) -> str: return self._render_prompt("system_prompt", locals()) diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index 8f3dd504..e912f7ca 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -5,32 +5,38 @@ prompts: system_prompt: | You are a professional coding agent concerned with one particular codebase. You have access to semantic coding tools on which you rely heavily for all your work, as well as collection of memory - files containing general information about the codebase. You operate in a frugal and intelligent manner, always + files containing general information about the codebase. You operate in a resource-efficient and intelligent manner, always keeping in mind to not read or generate content that is not needed for the task at hand. When reading code in order to answer a user question or task, you should try reading only the necessary code. Some tasks may require you to understand the architecture of large parts of the codebase, while for others, it may be enough to read a small set of symbols or a single file. Generally, you should avoid reading entire files unless it is absolutely necessary, instead relying on - intelligent step-by-step acquisition of information. However, if you already read a file, it does not make + intelligent step-by-step acquisition of information. {% if 'ToolMarkerSymbolicRead' in available_markers %}However, if you already read a file, it does not make sense to further analyse it with the symbolic tools (except for the `find_referencing_symbols` tool), - as you already have the information. + as you already have the information.{% endif %} - I WILL BE SERIOUSLY UPSET IF YOU READ ENTIRE FILES WITHOUT NEED! CONSIDER INSTEAD USING THE OVERVIEW TOOL AND - SYMBOLIC TOOLS TO READ ONLY THE NECESSARY CODE FIRST! + I WILL BE SERIOUSLY UPSET IF YOU READ ENTIRE FILES WITHOUT NEED! + {% if 'ToolMarkerSymbolicRead' in available_markers %} + CONSIDER INSTEAD USING THE OVERVIEW TOOL AND SYMBOLIC TOOLS TO READ ONLY THE NECESSARY CODE FIRST! I WILL BE EVEN MORE UPSET IF AFTER HAVING READ AN ENTIRE FILE YOU KEEP READING THE SAME CONTENT WITH THE SYMBOLIC TOOLS! THE PURPOSE OF THE SYMBOLIC TOOLS IS TO HAVE TO READ LESS CODE, NOT READ THE SAME CONTENT MULTIPLE TIMES! + {% endif %} You can achieve the intelligent reading of code by using the symbolic tools for getting an overview of symbols and the relations between them, and then only reading the bodies of symbols that are necessary to answer the question - or complete the task. You can also use the standard tools like list_dir, find_file and search_for_pattern if you need to. + or complete the task. + You can use the standard tools like list_dir, find_file and search_for_pattern if you need to. When tools allow it, you pass the `relative_path` parameter to restrict the search to a specific file or directory. For some tools, `relative_path` can only be a file path, so make sure to properly read the tool descriptions. - If you are unsure about a symbol's name or location (to the extent that substring_matching for the symbol name is not enough), you can use the `search_for_pattern` tool, which allows fast - and flexible search for patterns in the codebase. This way you can first find candidates for symbols or files, - and then proceed with the symbolic tools. + {% if 'search_for_pattern' in available_tools %} + If you are unsure about a symbol's name or location{% if 'find_symbol' in available_tools %} (to the extent that substring_matching for the symbol name is not enough){% endif %}, you can use the `search_for_pattern` tool, which allows fast + and flexible search for patterns in the codebase.{% if 'ToolMarkerSymbolicRead' in available_markers %}This way you can first find candidates for symbols or files, + and then proceed with the symbolic tools.{% endif %} + {% endif %} - Symbols are identified by their `name_path and `relative_path`, see the description of the `find_symbols` tool for more details + {% if 'ToolMarkerSymbolicRead' in available_markers %} + Symbols are identified by their `name_path and `relative_path`, see the description of the `find_symbol` tool for more details on how the `name_path` matches symbols. You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible @@ -40,10 +46,13 @@ prompts: you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding to read the desired methods with `include_body=True` You can understand relationships between symbols by using the `find_referencing_symbols` tool. + {% endif %} + {% if 'read_memory' in available_tools %} You generally have access to memories and it may be useful for you to read them, but also only if they help you to answer the question or complete the task. You can infer which memories are relevant to the current task by reading the memory names and descriptions. + {% endif %} The context and modes of operation are described below. From them you can infer how to interact with your user and which tasks and kinds of interactions are expected of you. diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 319aa590..8b9f2d1a 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -1,10 +1,10 @@ import json -from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerOptional +from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerOptional, ToolMarkerSymbolicRead from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient -class JetBrainsFindSymbolTool(Tool, ToolMarkerOptional): +class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). """ @@ -70,7 +70,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerOptional): return self._limit_length(result, max_answer_chars) -class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerOptional): +class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol """ @@ -100,7 +100,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerOptional): return self._limit_length(result, max_answer_chars) -class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerOptional): +class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Retrieves an overview of the top-level symbols within a specified file """ diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 4d2a5ffc..45e04f3e 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -8,7 +8,13 @@ from collections.abc import Sequence from copy import copy from typing import Any -from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerCanEdit +from serena.tools import ( + SUCCESS_RESULT, + TOOL_DEFAULT_MAX_ANSWER_LENGTH, + Tool, + ToolMarkerSymbolicEdit, + ToolMarkerSymbolicRead, +) from solidlsp.ls_types import SymbolKind @@ -42,7 +48,7 @@ class RestartLanguageServerTool(Tool): return SUCCESS_RESULT -class GetSymbolsOverviewTool(Tool): +class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead): """ Gets an overview of the top-level symbols defined in a given file or directory. """ @@ -70,7 +76,7 @@ class GetSymbolsOverviewTool(Tool): return self._limit_length(result_json_str, max_answer_chars) -class FindSymbolTool(Tool): +class FindSymbolTool(Tool, ToolMarkerSymbolicRead): """ Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). """ @@ -150,7 +156,7 @@ class FindSymbolTool(Tool): return self._limit_length(result, max_answer_chars) -class FindReferencingSymbolsTool(Tool): +class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead): """ Finds symbols that reference the symbol at the given location (optionally filtered by type). """ @@ -204,7 +210,7 @@ class FindReferencingSymbolsTool(Tool): return self._limit_length(result, max_answer_chars) -class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): +class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit): """ Replaces the full definition of a symbol. """ @@ -232,7 +238,7 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): +class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit): """ Inserts content after the end of the definition of a given symbol. """ @@ -257,7 +263,7 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): +class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit): """ Inserts content before the beginning of the definition of a given symbol. """ diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index cd025970..1f55e604 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -73,22 +73,40 @@ class Component(ABC): TOOL_DEFAULT_MAX_ANSWER_LENGTH = int(2e5) -class ToolMarkerCanEdit: +class ToolMarker: + """ + Base class for tool markers. + """ + + +class ToolMarkerCanEdit(ToolMarker): """ Marker class for all tools that can perform editing operations on files. """ -class ToolMarkerDoesNotRequireActiveProject: +class ToolMarkerDoesNotRequireActiveProject(ToolMarker): pass -class ToolMarkerOptional: +class ToolMarkerOptional(ToolMarker): """ Marker class for optional tools that are disabled by default. """ +class ToolMarkerSymbolicRead(ToolMarker): + """ + Marker class for tools that perform symbol read operations. + """ + + +class ToolMarkerSymbolicEdit(ToolMarkerCanEdit): + """ + Marker class for tools that perform symbolic edit operations. + """ + + class ApplyMethodProtocol(Protocol): """Callable protocol for the apply method of a tool.""" diff --git a/src/solidlsp/language_servers/solargraph.py b/src/solidlsp/language_servers/solargraph.py index 7b79c324..a4f7e694 100644 --- a/src/solidlsp/language_servers/solargraph.py +++ b/src/solidlsp/language_servers/solargraph.py @@ -7,7 +7,6 @@ import json import logging import os import pathlib -import stat import subprocess import threading From 1bff8fa77422c8566692a4d67050f411ffa7ef67 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 2 Aug 2025 20:42:47 +0200 Subject: [PATCH 41/49] Support use of available_tools and available_markers in context/mode prompts --- src/serena/agent.py | 43 +++++++++++++------ src/serena/config/context_mode.py | 8 ++++ src/serena/resources/config/modes/editing.yml | 8 ++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 5f27a1d1..96d769e8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Optional, TypeVar from sensai.util import logging from sensai.util.logging import LogTime +from interprompt.jinja_template import JinjaTemplate from serena import serena_version from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats from serena.config.context_mode import RegisteredContext, SerenaAgentContext, SerenaAgentMode @@ -92,6 +93,23 @@ class MemoriesManager: return f"Memory {name} deleted." +class AvailableTools: + def __init__(self, tools: list[Tool]): + """ + :param tools: the list of available tools + """ + self.tools = tools + self.tool_names = [tool.get_name_from_cls() for tool in tools] + self.tool_marker_names = set() + for marker_class in iter_subclasses(ToolMarker): + for tool in tools: + if isinstance(tool, marker_class): + self.tool_marker_names.add(marker_class.__name__) + + def __len__(self) -> int: + return len(self.tools) + + class SerenaAgent: def __init__( self, @@ -193,7 +211,7 @@ class SerenaAgent: tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains")) self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions) - self._exposed_tools = {tc: t for tc, t in self._all_tools.items() if self._base_tool_set.includes_name(t.get_name())} + self._exposed_tools = AvailableTools([t for t in self._all_tools.values() if self._base_tool_set.includes_name(t.get_name())]) log.info(f"Number of exposed tools: {len(self._exposed_tools)}") # create executor for starting the language server and running tools in another thread @@ -302,7 +320,7 @@ class SerenaAgent: If a client should attempt to use a tool that is dynamically disabled (e.g. because a project is activated that disables it), it will receive an error. """ - return list(self._exposed_tools.values()) + return list(self._exposed_tools.tools) def get_active_project(self) -> Project | None: """ @@ -336,24 +354,21 @@ class SerenaAgent: """ return list(self._modes) + def _format_prompt(self, prompt_template: str) -> str: + template = JinjaTemplate(prompt_template) + return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names) + def create_system_prompt(self) -> str: - available_tools = [tool.get_name_from_cls() for tool in self.get_exposed_tool_instances()] - - # determine available tool markers (i.e. tool categories) - available_markers = set() - for marker_class in iter_subclasses(ToolMarker): - for tool in self.get_exposed_tool_instances(): - if isinstance(tool, marker_class): - available_markers.add(marker_class.__name__) - + available_markers = self._exposed_tools.tool_marker_names log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers) system_prompt = self.prompt_factory.create_system_prompt( - context_system_prompt=self._context.prompt, - mode_system_prompts=[mode.prompt for mode in self._modes], - available_tools=available_tools, + context_system_prompt=self._format_prompt(self._context.prompt), + mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self._modes], + available_tools=self._exposed_tools.tool_names, available_markers=available_markers, ) log.info("System prompt:\n%s", system_prompt) + return system_prompt def _update_active_tools(self) -> None: """ diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index de84f6a7..394c609e 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -39,6 +39,10 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin): name: str prompt: str + """ + a Jinja2 template for the generation of the system prompt. + It is formatted by the agent (see SerenaAgent._format_prompt()). + """ description: str = "" def _tostring_includes(self) -> list[str]: @@ -133,6 +137,10 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): name: str prompt: str + """ + a Jinja2 template for the generation of the system prompt. + It is formatted by the agent (see SerenaAgent._format_prompt()). + """ description: str = "" def _tostring_includes(self) -> list[str]: diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index d53750fd..464f4537 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -14,7 +14,7 @@ prompt: | use the regex-based approach that is described below. Let us first discuss the symbol-based approach. - Symbols are identified by their name path and relative file path, see the description of the `find_symbols` tool for more details + Symbols are identified by their name path and relative file path, see the description of the `find_symbol` tool for more details on how the `name_path` matches symbols. You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible @@ -34,7 +34,8 @@ prompt: | You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well. You can assume that all symbol editing tools are reliable, so you don't need to verify the results if the tool returns without error. - Now let us discuss the regex-based approach. + {% if 'replace_regex' in available_tools %} + Let us discuss the regex-based approach. The regex-based approach is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation. This is the case if you need to adjust just a few lines of code within a method, or a chunk that is much smaller than a whole symbol. You use other tools to find the relevant content and @@ -104,7 +105,8 @@ prompt: | Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification! IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! -excluded_tools: + {% endif %} +excluded_tools: - replace_lines - insert_at_line - delete_lines From eb0f44cb793e2fb34603d53fb471396ca40e74f5 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 2 Aug 2025 20:03:19 +0200 Subject: [PATCH 42/49] Fixed appending of "." to docstring and mock in test... We should get rid of mocks eventually --- src/serena/mcp.py | 5 +++-- test/serena/test_mcp.py | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 6c75556f..fb42cbe9 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -68,8 +68,7 @@ class SerenaMCPFactory: # Mount the tool description as a combination of the docstring description and # the return value description, if it exists. - agent = cast(SerenaAgent, tool.agent) - overridden_description = agent.get_context().tool_description_overrides.get(func_name, None) + overridden_description = tool.agent.get_context().tool_description_overrides.get(func_name, None) if overridden_description is not None: func_doc = overridden_description @@ -78,6 +77,8 @@ class SerenaMCPFactory: else: func_doc = "" func_doc = func_doc.strip().strip(".") + if func_doc: + func_doc += "." if docstring.returns and (docstring_returns_descr := docstring.returns.description): # Only add a space before "Returns" if func_doc is not empty prefix = " " if func_doc else "" diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index 37b6eca8..731a1cc6 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -4,16 +4,28 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry +from serena.config.context_mode import SerenaAgentContext from serena.mcp import SerenaMCPFactory make_tool = SerenaMCPFactory.make_mcp_tool +# Create a mock agent for tool initialization +class MockAgent: + def __init__(self): + self.project_config = None + self.serena_config = None + + @staticmethod + def get_context() -> SerenaAgentContext: + return SerenaAgentContext.load_default() + + class BaseMockTool(Tool): """A mock Tool class for testing.""" - def __init__(self) -> None: - pass + def __init__(self): + super().__init__(MockAgent()) class BasicTool(BaseMockTool): @@ -274,13 +286,6 @@ def is_test_mock_class(tool_class: type) -> bool: @pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes()) def test_make_tool_all_tools(tool_class) -> None: """Test that make_tool works for all tools in the codebase.""" - - # Create a mock agent for tool initialization - class MockAgent: - def __init__(self): - self.project_config = None - self.serena_config = None - # Create an instance of the tool tool_instance = tool_class(MockAgent()) From 4410f37ab3b26f97a11903b1faa0e99f01c93742 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 2 Aug 2025 22:49:42 +0200 Subject: [PATCH 43/49] Context: back to using **data in from_yaml, remove to/from json_dict methods Needed since some fields come from parent class, easy to make mistakes (like I did in the past commit) --- src/serena/config/context_mode.py | 33 ++++--------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index 30caba0b..e04f3383 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -44,17 +44,6 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin): def _tostring_includes(self) -> list[str]: return ["name"] - def to_json_dict(self) -> dict[str, str | list[str]]: - result = asdict(self) - result["excluded_tools"] = list(result["excluded_tools"]) - return result - - @classmethod - def from_json_dict(cls, data: dict) -> Self: - data = copy(data) - data["excluded_tools"] = set(data["excluded_tools"]) - return cls(**data) - def print_overview(self) -> None: """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") @@ -140,30 +129,16 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): def _tostring_includes(self) -> list[str]: return ["name"] - def to_json_dict(self) -> dict[str, str | list[str]]: - result = asdict(self) - result["excluded_tools"] = list(result["excluded_tools"]) - return result - - @classmethod - def from_json_dict(cls, data: dict) -> Self: - data = copy(data) - data["excluded_tools"] = set(data["excluded_tools"]) - # Ensure backwards compatibility for tool_description_overrides - if "tool_description_overrides" not in data: - data["tool_description_overrides"] = {} - return cls(**data) - @classmethod def from_yaml(cls, yaml_path: str | Path) -> Self: """Load a context from a YAML file.""" with open(yaml_path, encoding="utf-8") as f: data = yaml.safe_load(f) name = data.pop("name", Path(yaml_path).stem) - prompt = data["prompt"] - description = data.get("description", "") - tool_description_overrides = data.get("tool_description_overrides", {}) - return cls(name=name, prompt=prompt, description=description, tool_description_overrides=tool_description_overrides) + # Ensure backwards compatibility for tool_description_overrides + if "tool_description_overrides" not in data: + data["tool_description_overrides"] = {} + return cls(name=name, **data) @classmethod def get_path(cls, name: str) -> str: From 99757f35ebd53173c397379d4884c70cacc3981e Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 00:14:11 +0200 Subject: [PATCH 44/49] Made more tools optional and shortened several docstrings --- docs/serena_on_chatgpt.md | 3 +- scripts/demo_run_tools.py | 10 ++--- src/serena/config/context_mode.py | 3 +- .../config/contexts/context.template.yml | 3 ++ .../resources/config/contexts/desktop-app.yml | 3 ++ .../config/contexts/ide-assistant.yml | 5 --- .../resources/config/modes/mode.template.yml | 3 ++ src/serena/tools/cmd_tools.py | 11 +---- src/serena/tools/config_tools.py | 8 ++-- src/serena/tools/file_tools.py | 20 +++------ src/serena/tools/memory_tools.py | 11 +---- src/serena/tools/symbol_tools.py | 44 +++++++++---------- src/serena/tools/workflow_tools.py | 2 +- 13 files changed, 52 insertions(+), 74 deletions(-) diff --git a/docs/serena_on_chatgpt.md b/docs/serena_on_chatgpt.md index 64f7e248..ec872696 100644 --- a/docs/serena_on_chatgpt.md +++ b/docs/serena_on_chatgpt.md @@ -18,7 +18,7 @@ Run the following command to launch Serena as http server (assuming port 8000): ```bash uvx mcpo --port 8000 --api-key -- \ uvx --from git+https://github.com/oraios/serena \ - serena start-mcp-server --project $(pwd) + serena start-mcp-server --context chatgpt --project $(pwd) ``` - `--api-key` is required to secure the server. @@ -63,6 +63,7 @@ Your server is now securely exposed to the internet. ``` "servers": ["url": ""], ``` + **Important**: don't include a trailing slash at the end of the URL! ChatGPT will read the schema and create functions automatically. diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 394fe553..d52cfb00 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -9,7 +9,7 @@ from pprint import pprint from serena.agent import SerenaAgent from serena.config.serena_config import SerenaConfig from serena.constants import REPO_ROOT -from serena.tools import FindFileTool, FindReferencingSymbolsTool, SearchForPatternTool +from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool if __name__ == "__main__": agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False)) @@ -18,13 +18,9 @@ if __name__ == "__main__": find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) find_file_tool = agent.get_tool(FindFileTool) search_pattern_tool = agent.get_tool(SearchForPatternTool) + overview_tool = agent.get_tool(GetSymbolsOverviewTool) result = agent.execute_task( - lambda: search_pattern_tool.apply( - r"def request_full_.*?\).*?\)", - restrict_search_to_code_files=False, - relative_path="src/solidlsp", - paths_include_glob="**/ls.py", - ) + lambda: overview_tool.apply("src/solidlsp/ls.py"), ) pprint(json.loads(result)) diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index e04f3383..fbe34091 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -3,8 +3,7 @@ Context and Mode configuration loader """ import os -from copy import copy -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self diff --git a/src/serena/resources/config/contexts/context.template.yml b/src/serena/resources/config/contexts/context.template.yml index f9dee588..c01a1177 100644 --- a/src/serena/resources/config/contexts/context.template.yml +++ b/src/serena/resources/config/contexts/context.template.yml @@ -3,6 +3,9 @@ description: Description of the context, not used in the code. prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context. excluded_tools: [] +# several tools are excluded by default and have to be explicitly included by the user +included_optional_tools: [] + # mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method). # Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason. tool_description_overrides: {} \ No newline at end of file diff --git a/src/serena/resources/config/contexts/desktop-app.yml b/src/serena/resources/config/contexts/desktop-app.yml index 8105ca8d..c3adcfb4 100644 --- a/src/serena/resources/config/contexts/desktop-app.yml +++ b/src/serena/resources/config/contexts/desktop-app.yml @@ -11,4 +11,7 @@ prompt: | a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions. excluded_tools: [] +included_optional_tools: + - switch_modes + tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index 14d65130..27a13673 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -19,13 +19,8 @@ prompt: | excluded_tools: - create_text_file - read_file - - delete_lines - - replace_lines - - insert_at_line - execute_shell_command - prepare_for_new_conversation - - summarize_changes - - get_current_config tool_description_overrides: {} diff --git a/src/serena/resources/config/modes/mode.template.yml b/src/serena/resources/config/modes/mode.template.yml index 431afdeb..6d836ca3 100644 --- a/src/serena/resources/config/modes/mode.template.yml +++ b/src/serena/resources/config/modes/mode.template.yml @@ -2,3 +2,6 @@ description: Description of the mode, not used in the code. prompt: Prompt that will form part of the message sent to the model when activating this mode. excluded_tools: [] + +# several tools are excluded by default and have to be explicitly included by the user +included_optional_tools: [] \ No newline at end of file diff --git a/src/serena/tools/cmd_tools.py b/src/serena/tools/cmd_tools.py index 0db0da08..214d54e2 100644 --- a/src/serena/tools/cmd_tools.py +++ b/src/serena/tools/cmd_tools.py @@ -19,15 +19,8 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit): max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Execute a shell command and return its output. - - IMPORTANT: you should always consider the memory about suggested shell commands before using this tool. - If this memory was not loaded in the current conversation, you should load it using the `read_memory` tool - before using this tool. - - You should have at least once looked at the suggested shell commands from the corresponding memory - created during the onboarding process before using this tool. - Never execute unsafe shell commands like `rm -rf /` or similar! Generally be very careful with deletions. + Execute a shell command and return its output. If there is a memory about suggested commands, read that first. + Never execute unsafe shell commands like `rm -rf /` or similar! :param command: the shell command to execute :param cwd: the working directory to execute the command in. If None, the project root will be used. diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 51b782ad..8a3ddeb7 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -1,7 +1,7 @@ import json from serena.config.context_mode import SerenaAgentMode -from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject +from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): @@ -35,7 +35,7 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): return result_str -class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): +class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional): """ Removes a project from the Serena configuration. """ @@ -50,7 +50,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): return f"Successfully removed project '{project_name}' from configuration." -class SwitchModesTool(Tool): +class SwitchModesTool(Tool, ToolMarkerOptional): """ Activates modes by providing a list of their names """ @@ -71,7 +71,7 @@ class SwitchModesTool(Tool): return result_str -class GetCurrentConfigTool(Tool): +class GetCurrentConfigTool(Tool, ToolMarkerOptional): """ Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. """ diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index f79ffe06..b5770ab5 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -14,7 +14,7 @@ from fnmatch import fnmatch from pathlib import Path from serena.text_utils import search_files -from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit +from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional from serena.util.file_system import scan_directory @@ -29,7 +29,6 @@ class ReadFileTool(Tool): """ Reads the given file or a chunk of it. Generally, symbolic operations like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for. - Reading the entire file is only recommended if there is no other way to get the content required for the task. :param relative_path: the relative path to the file to read :param start_line: the 0-based index of the first line to be retrieved. @@ -60,14 +59,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit): def apply(self, relative_path: str, content: str) -> str: """ - Write a new file (or overwrite an existing file). For existing files, it is strongly recommended - to use symbolic operations like replace_symbol_body or insert_after_symbol/insert_before_symbol, if possible. - You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations - are not the right choice for what you want to do. - - If ever used on an existing file, the content has to be the complete content of that file (so it - may never end with something like "The remaining content of the file is left unchanged."). - For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead. + Write a new file or overwrite an existing file. :param relative_path: the relative path to the file to create :param content: the (utf-8-encoded) content to write to the file @@ -200,7 +192,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class DeleteLinesTool(Tool, ToolMarkerCanEdit): +class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Deletes a range of lines within a file. """ @@ -228,7 +220,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class ReplaceLinesTool(Tool, ToolMarkerCanEdit): +class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Replaces a range of lines within a file with new content. """ @@ -259,7 +251,7 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class InsertAtLineTool(Tool, ToolMarkerCanEdit): +class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Inserts content at a given line in a file. """ @@ -351,7 +343,7 @@ class SearchForPatternTool(Tool): For example, for finding classes or methods from a name pattern. Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, which is why it is the default. - :return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested). + :return: A mapping of file paths to lists of matched consecutive lines. """ abs_path = os.path.join(self.get_project_root(), relative_path) if not os.path.exists(abs_path): diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index 63bba4fe..3cf611f0 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -10,15 +10,8 @@ class WriteMemoryTool(Tool): def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Write some information about this project that can be useful for future tasks to a memory. - Use markdown formatting for the content. - The information should be short and to the point. - The memory name should be meaningful, such that from the name you can infer what the information is about. - It is better to have multiple small memories than to have a single large one because - memories will be read one by one and we only ever want to read relevant memories. - - This tool is either called during the onboarding process or when you have identified - something worth remembering about the project from the past conversation. + Write some information about this project that can be useful for future tasks to a memory in md format. + The memory name should be meaningful. """ if len(content) > max_answer_chars: raise ValueError( diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 4d2a5ffc..0dfb41a3 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -4,6 +4,7 @@ Language server-related tools import dataclasses import json +import os from collections.abc import Sequence from copy import copy from typing import Any @@ -33,10 +34,7 @@ class RestartLanguageServerTool(Tool): def apply(self) -> str: """Use this tool only on explicit user request or after confirmation. - It may be necessary to restart the language server if the user performs edits - not through Serena, so the language server state becomes outdated and further editing attempts lead to errors. - - If such editing errors happen, you should suggest using this tool. + It may be necessary to restart the language server if it hangs. """ self.agent.reset_language_server() return SUCCESS_RESULT @@ -49,24 +47,28 @@ class GetSymbolsOverviewTool(Tool): def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Gets an overview of the given file or directory. - For each analyzed file, we list the top-level symbols in the file (name_path, kind). - Use this tool to get a high-level understanding of the code symbols. - Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols. - Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview - by first understanding the basic directory structure of the repository that you can get from memories - or by using the `list_dir` and `find_file` tools (or similar). + Use this tool to get a high-level understanding of the code symbols in a file. + This should be the first tool to call when you want to understand a new file, unless you already know + what you are looking for. :param relative_path: the relative path to the file or directory to get the overview of :param max_answer_chars: if the overview is longer than this number of characters, no content will be returned. Don't adjust unless there is really no other way to get the content required for the task. If the overview is too long, you should use a smaller directory instead, (e.g. a subdirectory). - :return: a JSON object mapping relative paths of all contained files to info about top-level symbols in the file (name_path, kind). + :return: a JSON object containing info about top-level symbols in the file """ symbol_retriever = self.create_language_server_symbol_retriever() - result = symbol_retriever.get_symbol_overview(relative_path) - result_json_str = json.dumps({k: [dataclasses.asdict(i) for i in l] for k, l in result.items()}) + file_path = os.path.join(self.project.project_root, relative_path) + + # The symbol overview is capable of working with both files and directories, + # but we want to ensure that the user provides a file path. + if not os.path.exists(file_path): + raise FileNotFoundError(f"File or directory {relative_path} does not exist in the project.") + if os.path.isdir(file_path): + raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ") + result = symbol_retriever.get_symbol_overview(relative_path)[relative_path] + result_json_str = json.dumps([dataclasses.asdict(i) for i in result]) return self._limit_length(result_json_str, max_answer_chars) @@ -132,7 +134,7 @@ class FindSymbolTool(Tool): :param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`. :param substring_matching: If True, use substring matching for the last segment of `name`. :param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned. - :return: JSON string: a list of symbols (with locations) matching the name. + :return: a list of symbols (with locations) matching the name. """ parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None @@ -164,10 +166,8 @@ class FindReferencingSymbolsTool(Tool): max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols - as well as a short code snippet around the reference (unless `include_body` is True, then the short snippet will be omitted). - Note that among other kinds of references, this function can be used to find (direct) subclasses of a class, - as subclasses are referencing symbols that have the kind class. + Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols + as well as a short code snippet around the reference. :param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol for which to find references. @@ -269,9 +269,9 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): body: str, ) -> str: """ - Inserts the given body/content before the beginning of the definition of the given symbol (via the symbol's location). - A typical use case is to insert a new class, function, method, field or variable assignment. - It also can be used to insert a new import statement before the first symbol in the file. + Inserts the given content before the beginning of the definition of the given symbol (via the symbol's location). + A typical use case is to insert a new class, function, method, field or variable assignment; or + a new import statement before the first symbol in the file. :param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 38cc19be..be78d03d 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -96,7 +96,7 @@ class ThinkAboutWhetherYouAreDoneTool(Tool): return self.prompt_factory.create_think_about_whether_you_are_done() -class SummarizeChangesTool(Tool): +class SummarizeChangesTool(Tool, ToolMarkerOptional): """ Provides instructions for summarizing the changes made to the codebase. """ From 3d8f5f7446508bd5cd68db826b66109ffa685524 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 00:14:23 +0200 Subject: [PATCH 45/49] Added chatgpt context --- .../resources/config/contexts/chatgpt.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/serena/resources/config/contexts/chatgpt.yml diff --git a/src/serena/resources/config/contexts/chatgpt.yml b/src/serena/resources/config/contexts/chatgpt.yml new file mode 100644 index 00000000..c758b2c3 --- /dev/null +++ b/src/serena/resources/config/contexts/chatgpt.yml @@ -0,0 +1,28 @@ +description: A configuration specific for chatgpt, which has a limit of 30 tools and requires short descriptions. +prompt: | + You are running in desktop app context where the tools give you access to the code base as well as some + access to the file system, if configured. You interact with the user through a chat interface that is separated + from the code base. As a consequence, if you are in interactive mode, your communication with the user should + involve high-level thinking and planning as well as some summarization of any code edits that you make. + For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth + between the chat and the code editor should be minimized as well as facilitated by you. + If complex changes have been made, advise the user on how to review them in the code editor. + If complex relationships that the user asked for should be visualized or explained, consider creating + a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering + options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions. +excluded_tools: [] +included_optional_tools: + - switch_modes + +tool_description_overrides: + find_symbol: | + Retrieves symbols matching `name_path` in a file. + Use `depth > 0` to include children. `name_path` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar" + replace_regex: | + Replaces text using regular expressions. Preferred for smaller edits where symbol-level tools aren't appropriate. + Use wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content. + Essential for multi-line replacements. + search_for_pattern: | + Flexible pattern search across codebase. Prefer symbolic operations when possible. + Uses DOTALL matching. Use non-greedy quantifiers (.*?) to avoid over-matching. + Supports file filtering via globs and code-only restriction. \ No newline at end of file From 069db44091afc90385606c72e07a5f6552b58b06 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 00:26:03 +0200 Subject: [PATCH 46/49] GH: fixing paths, hopefully cache starts working on unix --- .github/workflows/pytest.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 91dcd610..31f4b372 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -36,8 +36,8 @@ jobs: uses: actions/cache@v3 with: path: | - ~/go/bin - ~/.cache/go-build + ${{ env.HOME }}/go/bin + ${{ env.HOME }}/.cache/go-build key: go-binaries-${{ runner.os }}-gopls-latest - name: Install gopls if: steps.cache-go-binaries.outputs.cache-hit != 'true' @@ -77,7 +77,7 @@ jobs: id: cache-language-servers uses: actions/cache@v3 with: - path: ~/.serena/language_servers/static + path: ${{ env.HOME }}/.serena/language_servers/static key: language-servers-${{ runner.os }}-v1 restore-keys: | language-servers-${{ runner.os }}- From 7990c3f30d416321e3c69d57fe165c1be86d6d01 Mon Sep 17 00:00:00 2001 From: Kazuki Hanai Date: Sun, 3 Aug 2025 19:24:42 +0900 Subject: [PATCH 47/49] Fix: Add HTML escaping to dashboard log messages Escape special HTML characters in log messages to prevent XSS vulnerabilities and ensure proper display of code snippets containing HTML tags. --- src/serena/resources/dashboard/dashboard.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index b57226aa..318d0c16 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -1,5 +1,6 @@ class LogMessage { constructor(message, toolNames) { + message = this.escapeHtml(message); const logLevel = this.determineLogLevel(message); const highlightedMessage = this.highlightToolNames(message, toolNames); this.$elem = $('
').addClass('log-' + logLevel).html(highlightedMessage + '\n'); @@ -27,6 +28,21 @@ class LogMessage { }); return highlightedMessage; } + + escapeHtml (convertString) { + if (typeof convertString !== 'string') return convertString; + + const patterns = { + '<' : '<', + '>' : '>', + '&' : '&', + '"' : '"', + '\'' : ''', + '`' : '`' + }; + + return convertString.replace(/[<>&"'`]/g, match => patterns[match]); + }; } class Dashboard { From 574eb7b327a7c765c3cfca5d74b59f811140546b Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 13:35:33 +0200 Subject: [PATCH 48/49] GH: make sure cached dirs exist --- .github/workflows/pytest.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 31f4b372..3eeac6be 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -28,6 +28,12 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '>=1.17.0' + - name: Ensure cached directory exist before calling cache-related actions + shell: bash + run: | + mkdir -p $HOME/.serena/language_servers/static + mkdir -p $HOME/.cache/go-build + mkdir -p $HOME/go/bin # Add Go bin directory to PATH for this workflow # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH # Writing to this file adds the directory to the PATH for subsequent steps @@ -36,8 +42,8 @@ jobs: uses: actions/cache@v3 with: path: | - ${{ env.HOME }}/go/bin - ${{ env.HOME }}/.cache/go-build + ~/go/bin + ~/.cache/go-build key: go-binaries-${{ runner.os }}-gopls-latest - name: Install gopls if: steps.cache-go-binaries.outputs.cache-hit != 'true' @@ -49,7 +55,6 @@ jobs: with: elixir-version: '1.18.4' otp-version: '26.1' - - name: Prepare java uses: actions/setup-java@v3 with: @@ -77,7 +82,7 @@ jobs: id: cache-language-servers uses: actions/cache@v3 with: - path: ${{ env.HOME }}/.serena/language_servers/static + path: ~/.serena/language_servers/static key: language-servers-${{ runner.os }}-v1 restore-keys: | language-servers-${{ runner.os }}- From a5db2f46162896b37d87a6a9b619fbc96a119153 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 15:53:43 +0200 Subject: [PATCH 49/49] Permissions: set sane mode (755) instead of just executable for downloaded binaries --- src/solidlsp/language_servers/clangd_language_server.py | 3 +-- src/solidlsp/language_servers/clojure_lsp.py | 3 +-- src/solidlsp/language_servers/csharp_language_server.py | 5 ++--- src/solidlsp/language_servers/dart_language_server.py | 3 +-- src/solidlsp/language_servers/eclipse_jdtls.py | 3 +-- src/solidlsp/language_servers/omnisharp.py | 3 +-- src/solidlsp/language_servers/rust_analyzer.py | 3 +-- src/solidlsp/language_servers/terraform_ls.py | 3 +-- 8 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index a770d78e..3879fae7 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -5,7 +5,6 @@ Provides C/C++ specific instantiation of the LanguageServer class. Contains vari import logging import os import pathlib -import stat import threading from solidlsp.ls import SolidLanguageServer @@ -95,7 +94,7 @@ class ClangdLanguageServer(SolidLanguageServer): f"Clangd executable not found at {clangd_executable_path}.\n" "Make sure you have installed clangd. See https://clangd.llvm.org/installation" ) - os.chmod(clangd_executable_path, stat.S_IEXEC) + os.chmod(clangd_executable_path, 0o755) return clangd_executable_path diff --git a/src/solidlsp/language_servers/clojure_lsp.py b/src/solidlsp/language_servers/clojure_lsp.py index f973db15..d81fa236 100644 --- a/src/solidlsp/language_servers/clojure_lsp.py +++ b/src/solidlsp/language_servers/clojure_lsp.py @@ -6,7 +6,6 @@ import logging import os import pathlib import shutil -import stat import subprocess import threading @@ -118,7 +117,7 @@ class ClojureLSP(SolidLanguageServer): deps.install(logger, clojurelsp_ls_dir) if not os.path.exists(clojurelsp_executable_path): raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}") - os.chmod(clojurelsp_executable_path, stat.S_IEXEC) + os.chmod(clojurelsp_executable_path, 0o755) return clojurelsp_executable_path @staticmethod diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 66f8c3cb..f01d5b04 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -7,7 +7,6 @@ import logging import os import platform import shutil -import stat import subprocess import tarfile import threading @@ -314,7 +313,7 @@ class CSharpLanguageServer(SolidLanguageServer): # Make executable on Unix systems if platform.system().lower() != "windows": - server_dll.chmod(server_dll.stat().st_mode | stat.S_IEXEC) + server_dll.chmod(0o755) logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO) return str(server_dll) @@ -456,7 +455,7 @@ class CSharpLanguageServer(SolidLanguageServer): # Make dotnet executable on Unix if platform.system().lower() != "windows": - dotnet_exe.chmod(dotnet_exe.stat().st_mode | stat.S_IEXEC) + dotnet_exe.chmod(0o755) logger.log(f"Successfully installed .NET 9 runtime to {dotnet_exe}", logging.INFO) return str(dotnet_exe) diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index c9cfcad1..297294a4 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -1,7 +1,6 @@ import logging import os import pathlib -import stat from solidlsp.ls import SolidLanguageServer from solidlsp.ls_logger import LanguageServerLogger @@ -84,7 +83,7 @@ class DartLanguageServer(SolidLanguageServer): deps.install(logger, dart_ls_dir) assert os.path.exists(dart_executable_path) - os.chmod(dart_executable_path, stat.S_IEXEC) + os.chmod(dart_executable_path, 0o755) return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index bf87aab7..d664fb7a 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -7,7 +7,6 @@ import logging import os import pathlib import shutil -import stat import threading import uuid from pathlib import PurePath @@ -279,7 +278,7 @@ class EclipseJDTLS(SolidLanguageServer): ): FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"]) - os.chmod(jre_path, stat.S_IEXEC) + os.chmod(jre_path, 0o755) assert os.path.exists(vscode_java_path) assert os.path.exists(jre_home_path) diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index c9755fa8..e43287c5 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -6,7 +6,6 @@ import json import logging import os import pathlib -import stat import threading from collections.abc import Iterable @@ -194,7 +193,7 @@ class OmniSharp(SolidLanguageServer): FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip") omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"]) assert os.path.exists(omnisharp_executable_path) - os.chmod(omnisharp_executable_path, stat.S_IEXEC) + os.chmod(omnisharp_executable_path, 0o755) razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp") if not os.path.exists(razor_omnisharp_ls_dir): diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py index 48bae28a..308b653f 100644 --- a/src/solidlsp/language_servers/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -5,7 +5,6 @@ Provides Rust specific instantiation of the LanguageServer class. Contains vario import logging import os import pathlib -import stat import threading from overrides import override @@ -104,7 +103,7 @@ class RustAnalyzer(SolidLanguageServer): os.makedirs(rustanalyzer_ls_dir) deps.install(logger, rustanalyzer_ls_dir) assert os.path.exists(rustanalyzer_executable_path) - os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) + os.chmod(rustanalyzer_executable_path, 0o755) return rustanalyzer_executable_path diff --git a/src/solidlsp/language_servers/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls.py index e40d9af1..6a374df6 100644 --- a/src/solidlsp/language_servers/terraform_ls.py +++ b/src/solidlsp/language_servers/terraform_ls.py @@ -1,7 +1,6 @@ import logging import os import shutil -import stat import threading from overrides import override @@ -112,7 +111,7 @@ class TerraformLS(SolidLanguageServer): # Make the executable file executable on Unix-like systems if platform_id.value != "win-x64": - os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD) + os.chmod(terraform_ls_executable_path, 0o755) return terraform_ls_executable_path