From 2fe7daddb2d3f534e67473efc8aa490ccdb34514 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 9 Dec 2025 15:17:17 +0100 Subject: [PATCH] More flexible GetSymbolOverviewTool, allowing depth to be specified Simply use the dictionary conversion to generate overview elements, adding the new option to specify a predicate for filtering children (thus allowing low-level symbols like variables to be filtered out) --- src/serena/symbol.py | 64 +++++++++++++++++++++++--------- src/serena/tools/symbol_tools.py | 9 +++-- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 1640cafb..ecc13353 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -2,7 +2,7 @@ import json import logging import os from abc import ABC, abstractmethod -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from dataclasses import asdict, dataclass from typing import TYPE_CHECKING, Any, NotRequired, Self, TypedDict, Union @@ -209,6 +209,13 @@ class LanguageServerSymbol(Symbol, ToStringMixin): def symbol_kind(self) -> SymbolKind: return self.symbol_root["kind"] + def is_low_level(self) -> bool: + """ + :return: whether the symbol is a low-level symbol (variable, constant, etc.), which typically represents data + rather than structure and therefore is not relevant in a high-level overview of the code. + """ + return self.symbol_kind >= SymbolKind.Variable.value + @property def overload_idx(self) -> int | None: return self.symbol_root.get("overload_idx") @@ -380,13 +387,14 @@ class LanguageServerSymbol(Symbol, ToStringMixin): include_body: bool = False, include_children_body: bool = False, include_relative_path: bool = True, + child_inclusion_predicate: Callable[[Self], bool] | None = None, ) -> dict[str, Any]: """ Converts the symbol to a dictionary. :param kind: whether to include the kind of the symbol :param location: whether to include the location of the symbol - :param depth: the depth of the symbol + :param depth: the depth up to which to include child symbols (0 = do not include children) :param include_body: whether to include the body of the top-level symbol. :param include_children_body: whether to also include the body of the children. Note that the body of the children is part of the body of the parent symbol, @@ -394,6 +402,8 @@ class LanguageServerSymbol(Symbol, ToStringMixin): and pass the children without passing the parent body to the LM. :param include_relative_path: whether to include the relative path of the symbol in the location entry. Relative paths of the symbol's children are always excluded. + :param child_inclusion_predicate: an optional predicate that decides whether a child symbol + should be included. :return: a dictionary representation of the symbol """ result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()} @@ -411,14 +421,20 @@ class LanguageServerSymbol(Symbol, ToStringMixin): log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.") result["body"] = self.body - def add_children(s: Self) -> list[dict[str, Any]]: + if child_inclusion_predicate is None: + child_inclusion_predicate = lambda s: True + + def included_children(s: Self) -> list[dict[str, Any]]: children = [] for c in s.iter_children(): + if not child_inclusion_predicate(c): + continue children.append( c.to_dict( kind=kind, location=location, depth=depth - 1, + child_inclusion_predicate=child_inclusion_predicate, include_body=include_children_body, include_children_body=include_children_body, # all children have the same relative path as the parent @@ -428,7 +444,9 @@ class LanguageServerSymbol(Symbol, ToStringMixin): return children if depth > 0: - result["children"] = add_children(self) + children = included_children(self) + if len(children) > 0: + result["children"] = included_children(self) return result @@ -621,23 +639,35 @@ class LanguageServerSymbolRetriever: return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references] - @dataclass - class SymbolOverviewElement: - name_path: str - kind: int - - @classmethod - def from_symbol(cls, symbol: LanguageServerSymbol) -> Self: - return cls(name_path=symbol.get_name_path(), kind=int(symbol.symbol_kind)) - - def get_symbol_overview(self, relative_path: str) -> dict[str, list[SymbolOverviewElement]]: + def get_symbol_overview(self, relative_path: str, depth: int = 0) -> dict[str, list[dict]]: + """ + :param relative_path: the path of the file or directory for which to get the symbol overview + :param depth: the depth up to which to include child symbols (0 = only top-level symbols) + :return: a mapping from file paths to lists of symbol dictionaries. + For the case where a file is passed, the mapping will contain a single entry. + """ lang_server = self.get_language_server(relative_path) path_to_unified_symbols = lang_server.request_overview(relative_path) + + def child_inclusion_predicate(s: LanguageServerSymbol) -> bool: + return not s.is_low_level() + result = {} for file_path, unified_symbols in path_to_unified_symbols.items(): - # TODO: maybe include not just top-level symbols? We could filter by kind to exclude variables - # The language server methods would need to be adjusted for this. - result[file_path] = [self.SymbolOverviewElement.from_symbol(LanguageServerSymbol(s)) for s in unified_symbols] + symbols_in_file = [] + for us in unified_symbols: + symbol = LanguageServerSymbol(us) + symbols_in_file.append( + symbol.to_dict( + depth=depth, + kind=True, + include_relative_path=False, + location=False, + child_inclusion_predicate=child_inclusion_predicate, + ) + ) + result[file_path] = symbols_in_file + return result diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 72a73f48..75d0e5c9 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -2,7 +2,6 @@ Language server-related tools """ -import dataclasses import os from collections.abc import Sequence from copy import copy @@ -50,13 +49,15 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead): Gets an overview of the top-level symbols defined in a given file. """ - def apply(self, relative_path: str, max_answer_chars: int = -1) -> str: + def apply(self, relative_path: str, depth: int = 0, max_answer_chars: int = -1) -> str: """ 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 to get the overview of + :param depth: depth up to which descendants of top-level symbols shall be retrieved + (e.g. 1 retrieves immediate children). Default 0. :param max_answer_chars: if the overview is longer than this number of characters, no content will be returned. -1 means the default value from the config will be used. Don't adjust unless there is really no other way to get the content required for the task. @@ -71,8 +72,8 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead): 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 = self._to_json([dataclasses.asdict(i) for i in result]) + result = symbol_retriever.get_symbol_overview(relative_path, depth=depth)[relative_path] + result_json_str = self._to_json(result) return self._limit_length(result_json_str, max_answer_chars)