From 4514d04c90631dd5496fdcd0daf7299065732d55 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:04:54 +0200 Subject: [PATCH] Minor (docstrings, validation) --- src/multilspy/language_server.py | 33 ++++++++++++------ src/serena/agent.py | 58 +++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index c7f921d6..5bd0ae5e 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1760,12 +1760,15 @@ class SyncLanguageServer: Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is defined + :return: A list of Locations defining the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_definition(file_path, line, column), self.loop ).result(timeout=self.timeout) @@ -1773,15 +1776,18 @@ class SyncLanguageServer: def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: """ - Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. + Raises a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server + for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is referenced + :return: A list of Locations referencing the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + try: result = asyncio.run_coroutine_threadsafe( self.language_server.request_references(file_path, line, column), self.loop @@ -1867,6 +1873,9 @@ class SyncLanguageServer: :return: A list of root symbols representing the top-level packages/modules in the project. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop ).result(timeout=self.timeout) @@ -1879,7 +1888,9 @@ class SyncLanguageServer: Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - assert self.loop + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_dir_overview(relative_dir_path), self.loop ).result(timeout=self.timeout) diff --git a/src/serena/agent.py b/src/serena/agent.py index db300c5d..b8c177f3 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,7 +9,6 @@ import platform import re import shutil import sys -import threading import traceback import webbrowser from abc import ABC, abstractmethod @@ -21,7 +20,6 @@ from fnmatch import fnmatch from functools import cached_property from logging import Logger from pathlib import Path -from time import time from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast @@ -306,6 +304,59 @@ class SerenaConfigBase(ABC): else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") + def to_dict(self) -> dict: + """Convert configuration to dictionary for serialization.""" + return { + "projects": [ + { + "project_root": project.project_root, + "project_config": { + "project_name": project.project_config.project_name, + "language": project.project_config.language, + "ignored_paths": project.project_config.ignored_paths, + "ignore_all_files_in_gitignore": project.project_config.ignore_all_files_in_gitignore, + }, + } + for project in self.projects + ], + "gui_log_window_enabled": self.gui_log_window_enabled, + "log_level": self.log_level, + "trace_lsp_communication": self.trace_lsp_communication, + "web_dashboard": self.web_dashboard, + "tool_timeout": self.tool_timeout, + } + + @classmethod + def from_dict(cls, data: dict) -> "SerenaConfigBase": + """Create configuration from dictionary.""" + projects = [] + for project_data in data.get("projects", []): + project_config = ProjectConfig( + project_name=project_data["project_config"]["project_name"], + language=project_data["project_config"]["language"], + ignored_paths=project_data["project_config"]["ignored_paths"], + ignore_all_files_in_gitignore=project_data["project_config"]["ignore_all_files_in_gitignore"], + ) + project = Project(project_root=project_data["project_root"], project_config=project_config) + projects.append(project) + + # Create a basic config class that can be instantiated + from dataclasses import dataclass + + @dataclass(kw_only=True) + class DeserializedConfig(SerenaConfigBase): + def _add_new_project(self, project: Project) -> None: + self.projects.append(project) + + return DeserializedConfig( + projects=projects, + gui_log_window_enabled=data.get("gui_log_window_enabled", False), + log_level=data.get("log_level", logging.INFO), + trace_lsp_communication=data.get("trace_lsp_communication", False), + web_dashboard=data.get("web_dashboard", True), + tool_timeout=data.get("tool_timeout", DEFAULT_TOOL_TIMEOUT), + ) + @dataclass(kw_only=True) class SerenaConfig(SerenaConfigBase): @@ -851,14 +902,13 @@ class SerenaAgent: """ Starts/resets the language server for the current project """ - tool_timeout = self.serena_config.tool_timeout if tool_timeout is None or tool_timeout < 0: ls_timeout = None else: if tool_timeout < 10: raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") - ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout # stop the language server if it is running if self.is_language_server_running():