From b9764fce673357e00985bc35f96ed2abe248d71b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 28 May 2025 19:46:50 +0200 Subject: [PATCH] Symbol: added find, insert, replace and delete by name. Also added delete by location. Renamed name to name_path --- src/serena/agent.py | 8 +-- src/serena/symbol.py | 117 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 52b948c..8201286 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1202,7 +1202,7 @@ class FindReferencingSymbolsTool(Tool): """ 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 - symbols = self.symbol_manager.find_referencing_symbols( + symbols = self.symbol_manager.find_referencing_symbols_by_location( SymbolLocation(relative_path, line, column), include_body=include_body, include_kinds=parsed_include_kinds, @@ -1279,7 +1279,7 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): :param body: the new symbol body. Important: Provide the correct level of indentation (as the original body). Note that the first line must not be indented (i.e. no leading spaces). """ - self.symbol_manager.replace_body( + self.symbol_manager.replace_body_at_location( SymbolLocation(relative_path, line, column), body=body, ) @@ -1308,7 +1308,7 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): :param body: the body/content to be inserted """ location = SymbolLocation(relative_path, line, column) - self.symbol_manager.insert_after( + self.symbol_manager.insert_after_symbol_at_location( location, body=body, ) @@ -1337,7 +1337,7 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): :param column: the column :param body: the body/content to be inserted """ - self.symbol_manager.insert_before( + self.symbol_manager.insert_before_symbol_at_location( SymbolLocation(relative_path, line, column), body=body, ) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 3178d55..0616f2a 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -1,3 +1,4 @@ +import json import logging import os from collections.abc import Iterator, Sequence @@ -337,7 +338,7 @@ class SymbolManager: def find_by_name( self, - name: str, + name_path: str, include_body: bool = False, include_kinds: Sequence[SymbolKind] | None = None, exclude_kinds: Sequence[SymbolKind] | None = None, @@ -353,7 +354,9 @@ class SymbolManager: symbol_roots = self.lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) for root in symbol_roots: symbols.extend( - Symbol(root).find(name, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching) + Symbol(root).find( + name_path, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching + ) ) return symbols @@ -373,6 +376,35 @@ class SymbolManager: return None def find_referencing_symbols( + self, + name_path: str, + relative_file_path: str, + include_body: bool = False, + include_kinds: Sequence[SymbolKind] | None = None, + exclude_kinds: Sequence[SymbolKind] | None = None, + ) -> tuple[Symbol, list[Symbol]] | None: + """ + Find all symbols that reference the symbol with the given name. + If multiple symbols fit the name (e.g. for variables that are overwritten), will use the first one. + """ + symbol_candidates = self.find_by_name(name_path, substring_matching=False, within_relative_path=relative_file_path) + if len(symbol_candidates) == 0: + log.warning(f"No symbol with name {name_path} found in file {relative_file_path}") + return None + if len(symbol_candidates) > 1: + log.error( + f"Found {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}." + f"May be an overwritten variable, in which case you can ignore this error. Proceeding with the first one. " + f"Found symbols for {name_path=} in {relative_file_path=}: \n" + f"" + ) + symbol = symbol_candidates[0] + referencing_symbols = self.find_referencing_symbols_by_location( + symbol.location, include_body=include_body, include_kinds=include_kinds, exclude_kinds=exclude_kinds + ) + return symbol, referencing_symbols + + def find_referencing_symbols_by_location( self, symbol_location: SymbolLocation, include_body: bool = False, @@ -434,7 +466,24 @@ class SymbolManager: with self._edited_file(location.relative_path): yield symbol - def replace_body(self, location: SymbolLocation, body: str) -> None: + def replace_body(self, name_path: str, relative_file_path: str, body: str) -> None: + """ + Replace the body of the symbol with the given name in the given file. + """ + symbol_candidates = self.find_by_name(name_path, within_relative_path=relative_file_path) + if len(symbol_candidates) == 0: + raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}") + if len(symbol_candidates) > 1: + raise ValueError( + f"Found multiple {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}. " + "Will not replace the body of any of them, but you can use `replace_body_at_location`, the replace lines tool or other editing " + "tools to perform your edits. Their locations are: \n " + + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) + ) + symbol = symbol_candidates[0] + self.replace_body_at_location(symbol.location, body) + + def replace_body_at_location(self, location: SymbolLocation, body: str) -> None: """ Replace the body of the symbol at the given location with the given body @@ -454,7 +503,23 @@ class SymbolManager: self.lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) self.lang_server.insert_text_at_position(location.relative_path, start_pos["line"], start_pos["character"], body) - def insert_after(self, location: SymbolLocation, body: str) -> None: + def insert_after_symbol(self, name_path: str, relative_file_path: str, body: str) -> None: + """ + Inserts content after the symbol with the given name in the given file. + """ + symbol_candidates = self.find_by_name(name_path, within_relative_path=relative_file_path) + if len(symbol_candidates) == 0: + raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}") + if len(symbol_candidates) > 1: + raise ValueError( + f"Found multiple {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}. " + f"May be an overwritten variable, in which case you can ignore this error. Proceeding with the last one. " + f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) + ) + symbol = symbol_candidates[-1] + self.insert_after_symbol_at_location(symbol.location, body) + + def insert_after_symbol_at_location(self, location: SymbolLocation, body: str) -> None: """ Appends content after the given symbol @@ -472,7 +537,23 @@ class SymbolManager: assert location.relative_path is not None self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) - def insert_before(self, location: SymbolLocation, body: str) -> None: + def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str) -> None: + """ + Inserts content before the symbol with the given name in the given file. + """ + symbol_candidates = self.find_by_name(name_path, within_relative_path=relative_file_path) + if len(symbol_candidates) == 0: + raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}") + if len(symbol_candidates) > 1: + raise ValueError( + f"Found multiple {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}. " + f"May be an overwritten variable, in which case you can ignore this error. Proceeding with the first one. " + f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) + ) + symbol = symbol_candidates[0] + self.insert_before_symbol_at_location(symbol.location, body) + + def insert_before_symbol_at_location(self, location: SymbolLocation, body: str) -> None: """ Inserts content before the given symbol @@ -512,3 +593,29 @@ class SymbolManager: start_pos = Position(line=start_line, character=0) end_pos = Position(line=end_line + 1, character=0) self.lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) + + def delete_symbol_at_location(self, location: SymbolLocation) -> None: + """ + Deletes the symbol at the given location. + """ + with self._edited_symbol_location(location) as symbol: + assert location.relative_path is not None + assert symbol.body_start_position is not None + assert symbol.body_end_position is not None + self.lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) + + def delete_symbol(self, name_path: str, relative_file_path: str) -> None: + """ + Deletes the symbol with the given name in the given file. + """ + symbol_candidates = self.find_by_name(name_path, within_relative_path=relative_file_path) + if len(symbol_candidates) == 0: + raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}") + if len(symbol_candidates) > 1: + raise ValueError( + f"Found multiple {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}. " + "Will not delete any of them, but you can use `delete_symbol_at_location` or a corresponding tool to perform your edits. " + "Their locations are: \n " + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) + ) + symbol = symbol_candidates[0] + self.delete_symbol_at_location(symbol.location)