#![allow(dead_code)]
use super::ast_utils;
use crate::database::WindjammerDatabase;
use tower_lsp::lsp_types::*;
pub struct ChangeSignature<'a> {
db: &'a WindjammerDatabase,
uri: Url,
position: Position,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParameterChange {
Add {
name: String,
type_hint: Option<String>,
default_value: String,
index: usize,
},
Remove { index: usize },
Reorder { from: usize, to: usize },
Rename { index: usize, new_name: String },
}
#[derive(Debug, Clone)]
pub struct SignatureAnalysis {
pub function_name: String,
pub parameters: Vec<Parameter>,
pub signature_range: Range,
pub call_sites: Vec<CallSite>,
pub is_safe: bool,
pub unsafe_reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub type_hint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CallSite {
pub range: Range,
pub arguments: Vec<String>,
}
impl<'a> ChangeSignature<'a> {
pub fn new(db: &'a WindjammerDatabase, uri: Url, position: Position) -> Self {
Self { db, uri, position }
}
pub fn execute(
&self,
changes: &[ParameterChange],
source: &str,
) -> Result<WorkspaceEdit, String> {
let analysis = self.analyze_function(source)?;
if !analysis.is_safe {
return Err(analysis
.unsafe_reason
.unwrap_or_else(|| "Cannot change signature: unsafe".to_string()));
}
let (new_signature, param_mapping) = self.apply_changes(&analysis, changes)?;
let mut edits = vec![];
edits.push(TextEdit {
range: analysis.signature_range,
new_text: new_signature,
});
for call_site in &analysis.call_sites {
let new_args = self.reorder_arguments(&call_site.arguments, ¶m_mapping, changes)?;
edits.push(TextEdit {
range: call_site.range,
new_text: format!("{}({})", analysis.function_name, new_args.join(", ")),
});
}
let mut changes_map = std::collections::HashMap::new();
changes_map.insert(self.uri.clone(), edits);
Ok(WorkspaceEdit {
changes: Some(changes_map),
document_changes: None,
change_annotations: None,
})
}
fn analyze_function(&self, source: &str) -> Result<SignatureAnalysis, String> {
let (function_name, signature_range, parameters) = self.find_function_at_cursor(source)?;
let call_sites = self.find_call_sites(source, &function_name, signature_range)?;
let (is_safe, unsafe_reason) = self.check_safety(&function_name, &call_sites);
Ok(SignatureAnalysis {
function_name,
parameters,
signature_range,
call_sites,
is_safe,
unsafe_reason,
})
}
fn find_function_at_cursor(
&self,
source: &str,
) -> Result<(String, Range, Vec<Parameter>), String> {
let pattern = r"fn\s+(\w+)\s*\(([^)]*)\)";
let re = regex::Regex::new(pattern).map_err(|e| e.to_string())?;
let cursor_byte = ast_utils::position_to_byte_offset(source, self.position);
for captures in re.captures_iter(source) {
let full_match = captures.get(0).unwrap();
let start = full_match.start();
let end = full_match.end();
if cursor_byte >= start && cursor_byte <= end {
let function_name = captures.get(1).unwrap().as_str().to_string();
let params_str = captures.get(2).unwrap().as_str();
let start_pos = ast_utils::byte_offset_to_position(source, start);
let end_pos = ast_utils::byte_offset_to_position(source, end);
let signature_range = Range {
start: start_pos,
end: end_pos,
};
let parameters = self.parse_parameters(params_str);
return Ok((function_name, signature_range, parameters));
}
}
Err("No function found at cursor".to_string())
}
fn parse_parameters(&self, params_str: &str) -> Vec<Parameter> {
if params_str.trim().is_empty() {
return vec![];
}
params_str
.split(',')
.filter_map(|param| {
let param = param.trim();
if param.is_empty() {
return None;
}
if let Some(colon_pos) = param.find(':') {
let name = param[..colon_pos].trim().to_string();
let type_hint = Some(param[colon_pos + 1..].trim().to_string());
Some(Parameter { name, type_hint })
} else {
Some(Parameter {
name: param.to_string(),
type_hint: None,
})
}
})
.collect()
}
fn find_call_sites(
&self,
source: &str,
function_name: &str,
signature_range: Range,
) -> Result<Vec<CallSite>, String> {
let mut call_sites = vec![];
let pattern = format!(r"{}\s*\(([^)]*)\)", regex::escape(function_name));
let re = regex::Regex::new(&pattern).map_err(|e| e.to_string())?;
let sig_start = ast_utils::position_to_byte_offset(source, signature_range.start);
let sig_end = ast_utils::position_to_byte_offset(source, signature_range.end);
for captures in re.captures_iter(source) {
let full_match = captures.get(0).unwrap();
let args_match = captures.get(1).unwrap();
let start = full_match.start();
let end = full_match.end();
if start >= sig_start && end <= sig_end {
continue;
}
let start_pos = ast_utils::byte_offset_to_position(source, start);
let end_pos = ast_utils::byte_offset_to_position(source, end);
let arguments = self.parse_arguments(args_match.as_str());
call_sites.push(CallSite {
range: Range {
start: start_pos,
end: end_pos,
},
arguments,
});
}
Ok(call_sites)
}
fn parse_arguments(&self, args_str: &str) -> Vec<String> {
if args_str.trim().is_empty() {
return vec![];
}
args_str
.split(',')
.map(|arg| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.collect()
}
fn apply_changes(
&self,
analysis: &SignatureAnalysis,
changes: &[ParameterChange],
) -> Result<(String, Vec<usize>), String> {
let mut params = analysis.parameters.clone();
let mut mapping: Vec<usize> = (0..params.len()).collect();
for change in changes {
match change {
ParameterChange::Add {
name,
type_hint,
index,
..
} => {
if *index > params.len() {
return Err(format!("Invalid index: {}", index));
}
params.insert(
*index,
Parameter {
name: name.clone(),
type_hint: type_hint.clone(),
},
);
for m in mapping.iter_mut() {
if *m >= *index {
*m += 1;
}
}
}
ParameterChange::Remove { index } => {
if *index >= params.len() {
return Err(format!("Invalid index: {}", index));
}
params.remove(*index);
mapping.retain(|&m| m != *index);
for m in mapping.iter_mut() {
if *m > *index {
*m -= 1;
}
}
}
ParameterChange::Reorder { from, to } => {
if *from >= params.len() || *to >= params.len() {
return Err(format!("Invalid indices: {} -> {}", from, to));
}
let param = params.remove(*from);
params.insert(*to, param);
let old_mapping = mapping[*from];
mapping.remove(*from);
mapping.insert(*to, old_mapping);
}
ParameterChange::Rename { index, new_name } => {
if *index >= params.len() {
return Err(format!("Invalid index: {}", index));
}
params[*index].name = new_name.clone();
}
}
}
let params_str = params
.iter()
.map(|p| {
if let Some(ref type_hint) = p.type_hint {
format!("{}: {}", p.name, type_hint)
} else {
p.name.clone()
}
})
.collect::<Vec<_>>()
.join(", ");
let new_signature = format!("fn {}({})", analysis.function_name, params_str);
Ok((new_signature, mapping))
}
fn reorder_arguments(
&self,
original_args: &[String],
mapping: &[usize],
changes: &[ParameterChange],
) -> Result<Vec<String>, String> {
let mut param_count = original_args.len();
for change in changes {
match change {
ParameterChange::Add { .. } => param_count += 1,
ParameterChange::Remove { .. } => param_count -= 1,
_ => {}
}
}
let mut new_args = vec![String::new(); param_count];
for (new_idx, &old_idx) in mapping.iter().enumerate() {
if old_idx < original_args.len() && new_idx < new_args.len() {
new_args[new_idx] = original_args[old_idx].clone();
}
}
for change in changes {
if let ParameterChange::Add {
index,
default_value,
..
} = change
{
if *index < new_args.len() {
new_args[*index] = default_value.clone();
}
}
}
Ok(new_args)
}
fn check_safety(
&self,
_function_name: &str,
_call_sites: &[CallSite],
) -> (bool, Option<String>) {
(true, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_parameters() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 0,
};
let change_sig = ChangeSignature::new(&db, uri, position);
let params = change_sig.parse_parameters("x: int, y: string");
assert_eq!(params.len(), 2);
assert_eq!(params[0].name, "x");
assert_eq!(params[0].type_hint, Some("int".to_string()));
assert_eq!(params[1].name, "y");
assert_eq!(params[1].type_hint, Some("string".to_string()));
}
#[test]
fn test_parse_arguments() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 0,
};
let change_sig = ChangeSignature::new(&db, uri, position);
let args = change_sig.parse_arguments("42, \"hello\"");
assert_eq!(args.len(), 2);
assert_eq!(args[0], "42");
assert_eq!(args[1], "\"hello\"");
}
#[test]
fn test_apply_add_parameter() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 0,
};
let change_sig = ChangeSignature::new(&db, uri, position);
let analysis = SignatureAnalysis {
function_name: "test".to_string(),
parameters: vec![Parameter {
name: "x".to_string(),
type_hint: Some("int".to_string()),
}],
signature_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 10,
},
},
call_sites: vec![],
is_safe: true,
unsafe_reason: None,
};
let changes = vec![ParameterChange::Add {
name: "y".to_string(),
type_hint: Some("string".to_string()),
default_value: "\"default\"".to_string(),
index: 1,
}];
let (new_sig, _) = change_sig.apply_changes(&analysis, &changes).unwrap();
assert!(new_sig.contains("x: int"));
assert!(new_sig.contains("y: string"));
}
}