use std::path::{Path, PathBuf};
use opendev_tools_lsp::protocol::UnifiedSymbolInfo;
use serde_json::Value;
use crate::error::{SymbolError, ToolResult};
use crate::util::LangCategory;
pub fn handle_replace_symbol_body(arguments: &Value, workspace_root: &Path) -> ToolResult {
let symbol_name = match arguments.get("symbol_name").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => s,
_ => return ToolResult::err("Missing required argument: symbol_name"),
};
let file_path = match arguments.get("file_path").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => PathBuf::from(s),
_ => return ToolResult::err("Missing required argument: file_path"),
};
let new_body = match arguments.get("new_body").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => s,
_ => return ToolResult::err("Missing required argument: new_body"),
};
let preserve_signature = arguments
.get("preserve_signature")
.and_then(|v| v.as_bool())
.unwrap_or(true);
if !file_path.exists() {
return ToolResult::err(format!("File not found: {}", file_path.display()));
}
match find_and_replace_body(
symbol_name,
&file_path,
new_body,
preserve_signature,
workspace_root,
) {
Ok(info) => ToolResult::ok_with(
format!(
"Replaced body of {} `{}` in {}",
info.kind.display_name(),
info.name,
file_path.display()
),
serde_json::json!({
"file_path": file_path.display().to_string(),
"symbol_name": info.name,
"symbol_kind": info.kind.display_name(),
}),
),
Err(e) => ToolResult::err(e.to_string()),
}
}
fn find_and_replace_body(
symbol_name: &str,
file_path: &Path,
_new_body: &str,
_preserve_signature: bool,
_workspace_root: &Path,
) -> Result<UnifiedSymbolInfo, SymbolError> {
Err(SymbolError::SymbolNotFound(format!(
"'{}' in {}",
symbol_name,
file_path.display()
)))
}
pub fn find_body_start(
lines: &[&str],
start_line: usize,
start_char: usize,
lang: LangCategory,
) -> Option<(usize, usize)> {
match lang {
LangCategory::Python => find_body_start_python(lines, start_line, start_char),
LangCategory::CLike => find_body_start_c_like(lines, start_line, start_char),
LangCategory::Other => None,
}
}
fn find_body_start_python(
lines: &[&str],
start_line: usize,
_start_char: usize,
) -> Option<(usize, usize)> {
for i in start_line..lines.len() {
let line = lines[i];
if let Some(colon_pos) = line.find(':') {
let after_colon = &line[colon_pos + 1..].trim();
if !after_colon.is_empty() && !after_colon.starts_with('#') {
return Some((
i,
colon_pos
+ 1
+ (line.len()
- line[colon_pos + 1..].len()
- line[colon_pos + 1..].trim_start().len()),
));
}
let mut body_line = i + 1;
while body_line < lines.len() {
let trimmed = lines[body_line].trim();
if trimmed.is_empty() {
body_line += 1;
continue;
}
if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
let quote = &trimmed[..3];
if trimmed.len() > 3 && trimmed[3..].contains(quote) {
body_line += 1;
continue;
}
body_line += 1;
while body_line < lines.len() {
if lines[body_line].contains(quote) {
body_line += 1;
break;
}
body_line += 1;
}
continue;
}
let indent = lines[body_line].len() - lines[body_line].trim_start().len();
return Some((body_line, indent));
}
return None;
}
}
None
}
fn find_body_start_c_like(
lines: &[&str],
start_line: usize,
_start_char: usize,
) -> Option<(usize, usize)> {
for i in start_line..lines.len() {
if let Some(brace_pos) = lines[i].find('{') {
let after = &lines[i][brace_pos + 1..].trim();
if !after.is_empty() {
return Some((i, brace_pos + 1));
}
if i + 1 < lines.len() {
let indent = lines[i + 1].len() - lines[i + 1].trim_start().len();
return Some((i + 1, indent));
}
return Some((i, brace_pos + 1));
}
}
None
}
pub fn replace_range(
lines: &[&str],
start_line: usize,
start_char: usize,
end_line: usize,
end_char: usize,
new_body: &str,
preserve_signature: bool,
) -> String {
let mut result = String::new();
for line in &lines[..start_line] {
result.push_str(line);
result.push('\n');
}
if preserve_signature && start_line < lines.len() {
let sig_line = lines[start_line];
let safe_start = start_char.min(sig_line.len());
result.push_str(&sig_line[..safe_start]);
if !result.ends_with('\n') && !new_body.starts_with('\n') {
result.push('\n');
}
}
result.push_str(new_body);
if !new_body.ends_with('\n') {
result.push('\n');
}
if end_line < lines.len() {
let end_line_content = lines[end_line];
let safe_end = end_char.min(end_line_content.len());
let remainder = &end_line_content[safe_end..];
if !remainder.is_empty() {
result.push_str(remainder);
result.push('\n');
}
}
for line in lines.iter().skip(end_line + 1) {
result.push_str(line);
result.push('\n');
}
result
}
#[cfg(test)]
#[path = "replace_body_tests.rs"]
mod tests;