Add and use use_same_indentation flag to body-replacement

This commit is contained in:
Michael Panchenko
2025-06-01 23:26:11 +02:00
parent 03dc1d62b9
commit 950caaf91c
3 changed files with 58 additions and 23 deletions
+28 -11
View File
@@ -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)
@@ -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);
}
}
+11 -6
View File
@@ -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);
}
"""