From 1afe45805f03d40e6674812ce1ce4b6d3f0c315e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 26 Mar 2025 22:57:26 +0100 Subject: [PATCH] New tool and LS method: get_document_overview --- src/multilspy/language_server.py | 37 +++++++++++++++++++------ src/multilspy/type_helpers.py | 2 ++ src/serena/mcp.py | 21 ++++++++++++++ test/multilspy/test_symbol_retrieval.py | 9 ++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 5e4e5a89..33850a44 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -843,16 +843,7 @@ class LanguageServer: Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - if not self.server_started: - self.logger.log( - "request_dir_overview called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - # Get the full symbol tree for the directory symbol_tree = await self.request_full_symbol_tree(relative_dir_path) - # Initialize result dictionary result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) @@ -879,6 +870,22 @@ class LanguageServer: process_symbol(root) return result + async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + """ + An overview of the given file. + Returns the the list of tuples (name, kind, line, column) of all top-level symbols in the file. + """ + _, document_roots = await self.request_document_symbols(relative_file_path) + return [ + ( + root["name"], + root["kind"], + root["selectionRange"]["start"]["line"], # type: ignore + root["selectionRange"]["start"]["character"] # type: ignore + ) + for root in document_roots + ] + async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: """ Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server @@ -1528,6 +1535,18 @@ class SyncLanguageServer: self.language_server.request_dir_overview(relative_dir_path), self.loop ).result() return result + + def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + """ + An overview of the given file. + + Returns the the list of tuples (name, kind, line, column) of all top-level symbols in the file. + """ + assert self.loop + result = asyncio.run_coroutine_threadsafe( + self.language_server.request_document_overview(relative_file_path), self.loop + ).result() + return result def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: """ diff --git a/src/multilspy/type_helpers.py b/src/multilspy/type_helpers.py index 307f7d43..0a805e43 100644 --- a/src/multilspy/type_helpers.py +++ b/src/multilspy/type_helpers.py @@ -17,6 +17,8 @@ def ensure_all_methods_implemented( def check_all_methods_implemented(target_cls: R) -> R: for name, _ in inspect.getmembers(source_cls, inspect.isfunction): + if name.startswith("_"): + continue if name not in target_cls.__dict__ or not callable(target_cls.__dict__[name]): raise NotImplementedError(f"{name} is not implemented in {target_cls}") diff --git a/src/serena/mcp.py b/src/serena/mcp.py index ed9a957f..d47cbf8b 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -244,6 +244,27 @@ def get_dir_overview(ctx: Context, relative_path: str, max_answer_chars: int = _ return GetDirOverviewTool(ctx).execute(max_answer_chars=max_answer_chars) +@mcp.tool() +def get_document_overview(ctx: Context, relative_path: str, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH) -> str: + """ + Get an overview of the given file. + + :param ctx: the context object, which will be created and provided automatically + :param relative_path: the relative path to the file 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. + :return: a JSON object with the list of tuples (name, kind, line, column) of all top-level symbols in the file. + """ + log.info(f"get_document_overview: {relative_path=}") + + class GetDocumentOverviewTool(Tool): + def _execute(self) -> str: + return json.dumps(self.langsrv.request_document_overview(relative_path)) + + return GetDocumentOverviewTool(ctx).execute(max_answer_chars=max_answer_chars) + + @mcp.tool() def find_symbol( ctx: Context, diff --git a/test/multilspy/test_symbol_retrieval.py b/test/multilspy/test_symbol_retrieval.py index 82372b0e..63a92c2a 100644 --- a/test/multilspy/test_symbol_retrieval.py +++ b/test/multilspy/test_symbol_retrieval.py @@ -407,3 +407,12 @@ class TestLanguageServerSymbols: for symbol in expected_symbols: assert symbol in services_symbols + + def test_request_document_overview(self, language_server: SyncLanguageServer, repo_path: Path): + """Test that request_document_overview returns correct symbol information for a file.""" + # Get overview of the user_management.py file + overview = language_server.request_document_overview(os.path.join("examples", "user_management.py")) + + # Verify that we have entries for both files + symbol_names = {s_info[0] for s_info in overview} + assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)