From 918ff32dcc9541e82e7d2377a97195d0497f4a0f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 15:59:40 +0200 Subject: [PATCH] Added an indexing command as uv script --- pyproject.toml | 1 + src/multilspy/language_server.py | 10 +++++++--- src/serena/agent.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a12c10cf..7e18dec7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ [project.scripts] serena-mcp-server = "serena.mcp:start_mcp_server" +index-project = "serena.agent:index_project" [project.license] text = "MIT" diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index f45c8f46..981769b7 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1607,7 +1607,7 @@ class LanguageServer: """ parsed_files = await self.request_parsed_files() files_processed = 0 - for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar): + for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar, desc="Indexing (files)"): await self.request_document_symbols(relative_file_path, include_body=False) await self.request_document_symbols(relative_file_path, include_body=True) files_processed += 1 @@ -2205,7 +2205,7 @@ class SyncLanguageServer: self.language_server.logger.log("Async shutdown tasks completed, returning to main thread", logging.DEBUG) # Note: NOT calling loop.stop() here - let main thread do it - def stop(self, shutdown_timeout: float = 5.0) -> None: + def stop(self, shutdown_timeout: float = 2.0) -> None: """ Stops the language server and robustly cleans up all associated resources, including the asyncio event loop, to prevent hangs on process exit. @@ -2230,26 +2230,30 @@ class SyncLanguageServer: if not is_windows: # 2. Graceful shutdown for Linux/Unix - try proper async cleanup first + self.language_server.logger.log("Starting graceful shutdown", logging.INFO) shutdown_future = None if self.loop and self.loop.is_running(): shutdown_future = asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) # 3. Wait for the background thread to exit if self.loop_thread: + self.language_server.logger.log("Waiting for event loop thread to exit", logging.INFO) self.loop_thread.join(timeout=shutdown_timeout) if self.loop_thread.is_alive(): self.language_server.logger.log("Event loop thread did not terminate within timeout", logging.WARNING) # 4. Wait for the shutdown coroutine to complete if it was scheduled if shutdown_future: + self.language_server.logger.log("Waiting for async shutdown to complete", logging.INFO) try: - shutdown_future.result(timeout=2.0) + shutdown_future.result(timeout=shutdown_timeout) self.language_server.logger.log("Graceful async shutdown completed", logging.DEBUG) except Exception as e: self.language_server.logger.log(f"Async shutdown failed: {e}", logging.WARNING) # 5. Close the loop properly after graceful shutdown if self.loop and not self.loop.is_closed(): + self.language_server.logger.log("Closing event loop", logging.INFO) try: self.loop.close() self.language_server.logger.log("Event loop closed successfully", logging.DEBUG) diff --git a/src/serena/agent.py b/src/serena/agent.py index 00760c1b..b1df6da2 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -23,6 +23,7 @@ from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast +import click import yaml from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from overrides import override @@ -656,6 +657,7 @@ def create_ls_for_project( trace_lsp_communication=trace_lsp_communication, ) ls_logger = MultilspyLogger(log_level=log_level) + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=log_level, force=True) log.info(f"Creating language server instance for {project_instance.project_root}.") return SyncLanguageServer.create( multilspy_config, @@ -665,6 +667,23 @@ def create_ls_for_project( ) +@click.command() +@click.argument("project", type=click.Path(exists=True)) +@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING") +def index_project(project: str, log_level: str = "INFO") -> None: + """ + Index a project by saving the symbols of files to Serena's language server cache. + + :param project: the project to index + """ + log_level_int = logging.getLevelNamesMapping()[log_level.upper()] + project = os.path.abspath(project) + log.info(f"Indexing project {project}") + ls = create_ls_for_project(project, log_level=log_level_int) + with ls.start_server(): + ls.index_repository() + + class SerenaAgent: def __init__( self,