From 950caaf91cd29690e877aee12ad3ce56e40c559f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 1 Jun 2025 23:26:11 +0200 Subject: [PATCH] Add and use use_same_indentation flag to body-replacement --- src/serena/symbol.py | 39 +++++++++++++------ .../__snapshots__/test_symbol_editing.ambr | 25 +++++++++--- test/serena/test_symbol_editing.py | 17 +++++--- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 9f96df3..e566f7f 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -643,9 +643,15 @@ class SymbolManager: """Get the content of a file using the language server.""" return self.lang_server.language_server.retrieve_full_file_content(relative_path) - def replace_body(self, name_path: str, relative_file_path: str, body: str) -> None: + def replace_body(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ - Replace the body of the symbol with the given name in the given file. + Replace the body of the symbol with the given name_path in the given file. + + :param name_path: the name path of the symbol to replace. + :param relative_file_path: the relative path of the file in which the symbol is defined. + :param body: the new body + :param use_same_indentation: whether to use the same indentation as the original body. This means that + the user doesn't have to provide the correct indentation, but can just write the body. """ symbol_candidates = self.find_by_name(name_path, within_relative_path=relative_file_path) if len(symbol_candidates) == 0: @@ -658,27 +664,33 @@ class SymbolManager: + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) ) symbol = symbol_candidates[0] - return self.replace_body_at_location(symbol.location, body) + return self.replace_body_at_location(symbol.location, body, use_same_indentation=use_same_indentation) - def replace_body_at_location(self, location: SymbolLocation, body: str) -> None: + def replace_body_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None: """ Replace the body of the symbol at the given location with the given body :param location: the location of the symbol to replace. :param body: the new body + :param use_same_indentation: whether to use the same indentation as the original body. This means that + the user doesn't have to provide the correct indentation, but can just write the body. """ - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" - with self._edited_symbol_location(location) as symbol: assert location.relative_path is not None start_pos = symbol.body_start_position end_pos = symbol.body_end_position if start_pos is None or end_pos is None: raise ValueError(f"Symbol at {location} does not have a defined body range.") + start_line, start_col = start_pos["line"], start_pos["character"] + if use_same_indentation: + indent = " " * start_col + body = "\n".join(indent + line for line in body.splitlines()) + + # make sure body always ends with at least one newline + if not body.endswith("\n"): + body += "\n" 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) + self.lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) def insert_after_symbol( self, @@ -731,14 +743,17 @@ class SymbolManager: raise ValueError(f"Symbol at {location} does not have a defined end position.") line, col = pos["line"], pos["character"] + if at_new_line: + line += 1 + col = 0 + if not body.startswith("\n"): + body = "\n" + body if use_same_indentation: symbol_start_pos = symbol.body_start_position assert symbol_start_pos is not None, f"Symbol at {location=} does not have a defined start position." symbol_identifier_col = symbol_start_pos["character"] indent = " " * (symbol_identifier_col) body = "\n".join(indent + line for line in body.splitlines()) - if at_new_line: - line += 1 # IMPORTANT: without this, the insertion does the wrong thing. See implementation of insert_text_at_position in TextUtils, # it is somewhat counterintuitive (never inserts whitespace) # I am not 100% sure whether col=0 is always the best choice here. @@ -807,6 +822,8 @@ class SymbolManager: if at_new_line: col = 0 line -= 1 + if not body.endswith("\n"): + body += "\n" assert location.relative_path is not None self.lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index c2f3716..4fa7309 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -100,6 +100,7 @@ # Module-level variable with type annotation typed_module_var: int = 42 + new_module_var = "Inserted after typed_module_var" # Regular class with class and instance variables @@ -199,7 +200,8 @@ reassignable_module_var = 10 reassignable_module_var = 20 # Reassigned - new_module_var = "Inserted after typed_module_var"# Module-level variable with type annotation + new_module_var = "Inserted after typed_module_var" + # Module-level variable with type annotation typed_module_var: int = 42 @@ -365,6 +367,7 @@ result = module_var + " used in function" other_result = reassignable_module_var * 2 return result, other_result + def new_inserted_function(): print("This is a new function inserted before another.") @@ -462,7 +465,8 @@ def new_inserted_function(): - print("This is a new function inserted before another.")# Function that uses the module variables + print("This is a new function inserted before another.") + # Function that uses the module variables def use_module_variables(): """Function that uses module-level variables.""" result = module_var + " used in function" @@ -495,9 +499,10 @@ console.log(this.value); } } + function newFunctionAfterClass(): void { console.log("This function is after DemoClass."); - # } + } export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); @@ -511,7 +516,8 @@ ''' function newFunctionAfterClass(): void { console.log("This function is after DemoClass."); - # }export class DemoClass { + } + export class DemoClass { value: number; constructor(value: number) { this.value = value; @@ -546,6 +552,7 @@ const demo = new DemoClass(42); demo.printValue(); } + function newInsertedFunction(): void { console.log("This is a new function inserted before another."); } @@ -567,6 +574,7 @@ function newInsertedFunction(): void { console.log("This is a new function inserted before another."); } + export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); @@ -621,7 +629,9 @@ # Instance variable with type annotation self.typed_instance_var: list[str] = ["item1", "item2"] - # This body has been replaced + + def modify_instance_var(self): + # This body has been replaced self.instance_var = "Replaced!" self.reassignable_instance_var = 999 # Reassigned @@ -684,8 +694,11 @@ constructor(value: number) { this.value = value; } - // This body has been replaced + + function printValue() { + // This body has been replaced console.warn("New value: " + this.value); + } } diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index 6c9c35b..6b86fe8 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -119,7 +119,7 @@ NEW_PYTHON_VARIABLE = 'new_module_var = "Inserted after typed_module_var"' NEW_TYPESCRIPT_FUNCTION_AFTER = """function newFunctionAfterClass(): void { console.log("This function is after DemoClass."); -# }""" +}""" class InsertInRelToSymbolTest(EditingTest): @@ -187,13 +187,18 @@ def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Liter test_case.run_test(content_after_ground_truth=snapshot) -PYTHON_REPLACED_BODY = """ # This body has been replaced - self.instance_var = "Replaced!" - self.reassignable_instance_var = 999 +PYTHON_REPLACED_BODY = """ +def modify_instance_var(self): + # This body has been replaced + self.instance_var = "Replaced!" + self.reassignable_instance_var = 999 """ -TYPESCRIPT_REPLACED_BODY = """ // This body has been replaced - console.warn("New value: " + this.value); +TYPESCRIPT_REPLACED_BODY = """ +function printValue() { + // This body has been replaced + console.warn("New value: " + this.value); +} """