#![allow(dead_code)]
use super::ast_utils;
use crate::database::WindjammerDatabase;
use tower_lsp::lsp_types::*;
pub struct InlineRefactoring<'a> {
db: &'a WindjammerDatabase,
uri: Url,
position: Position,
}
#[derive(Debug, Clone)]
pub struct InlineAnalysis {
pub name: String,
pub value: String,
pub definition_range: Range,
pub usage_ranges: Vec<Range>,
pub is_safe: bool,
pub unsafe_reason: Option<String>,
}
impl<'a> InlineRefactoring<'a> {
pub fn new(db: &'a WindjammerDatabase, uri: Url, position: Position) -> Self {
Self { db, uri, position }
}
pub fn execute(&self, source: &str) -> Result<WorkspaceEdit, String> {
let analysis = self.analyze_variable(source)?;
if !analysis.is_safe {
return Err(analysis
.unsafe_reason
.unwrap_or_else(|| "Cannot inline: unsafe".to_string()));
}
let mut edits = vec![];
for usage_range in &analysis.usage_ranges {
edits.push(TextEdit {
range: *usage_range,
new_text: analysis.value.clone(),
});
}
edits.push(TextEdit {
range: analysis.definition_range,
new_text: String::new(),
});
let mut changes = std::collections::HashMap::new();
changes.insert(self.uri.clone(), edits);
Ok(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
})
}
fn analyze_variable(&self, source: &str) -> Result<InlineAnalysis, String> {
let byte_offset = ast_utils::position_to_byte_offset(source, self.position);
let word = self.extract_word_at_offset(source, byte_offset)?;
let (def_range, value) = self.find_definition(source, &word)?;
let usage_ranges = self.find_usages(source, &word, def_range);
let (is_safe, unsafe_reason) = self.check_safety(source, &word, &value);
Ok(InlineAnalysis {
name: word,
value,
definition_range: def_range,
usage_ranges,
is_safe,
unsafe_reason,
})
}
fn extract_word_at_offset(&self, source: &str, offset: usize) -> Result<String, String> {
let bytes = source.as_bytes();
if offset >= bytes.len() {
return Err("Position out of bounds".to_string());
}
let mut start = offset;
let mut end = offset;
while start > 0 {
let ch = bytes[start - 1] as char;
if ch.is_alphanumeric() || ch == '_' {
start -= 1;
} else {
break;
}
}
while end < bytes.len() {
let ch = bytes[end] as char;
if ch.is_alphanumeric() || ch == '_' {
end += 1;
} else {
break;
}
}
if start == end {
return Err("No identifier at cursor".to_string());
}
Ok(source[start..end].to_string())
}
fn find_definition(&self, source: &str, name: &str) -> Result<(Range, String), String> {
let pattern = format!(r"let\s+{}\s*=\s*([^;\n]+)", regex::escape(name));
let re = regex::Regex::new(&pattern).map_err(|e| e.to_string())?;
if let Some(captures) = re.captures(source) {
let full_match = captures.get(0).unwrap();
let value_match = captures.get(1).unwrap();
let start_pos = ast_utils::byte_offset_to_position(source, full_match.start());
let end_pos = ast_utils::byte_offset_to_position(source, full_match.end());
let end_line = end_pos.line + 1;
let range = Range {
start: Position {
line: start_pos.line,
character: 0,
},
end: Position {
line: end_line,
character: 0,
},
};
Ok((range, value_match.as_str().trim().to_string()))
} else {
Err(format!("Could not find definition of '{}'", name))
}
}
fn find_usages(&self, source: &str, name: &str, def_range: Range) -> Vec<Range> {
let mut usages = vec![];
let def_start_byte = ast_utils::position_to_byte_offset(source, def_range.start);
let def_end_byte = ast_utils::position_to_byte_offset(source, def_range.end);
let pattern = format!(r"\b{}\b", regex::escape(name));
if let Ok(re) = regex::Regex::new(&pattern) {
for m in re.find_iter(source) {
let match_start = m.start();
let match_end = m.end();
if match_start >= def_start_byte && match_end <= def_end_byte {
continue;
}
let start_pos = ast_utils::byte_offset_to_position(source, match_start);
let end_pos = ast_utils::byte_offset_to_position(source, match_end);
usages.push(Range {
start: start_pos,
end: end_pos,
});
}
}
usages
}
fn check_safety(&self, _source: &str, _name: &str, value: &str) -> (bool, Option<String>) {
if value.contains("(") && (value.contains("!") || value.contains("await")) {
return (
false,
Some("Cannot inline: expression may have side effects".to_string()),
);
}
if value.len() > 100 {
return (
false,
Some("Cannot inline: expression too complex".to_string()),
);
}
if value.contains("=") && !value.contains("==") && !value.contains("!=") {
return (
false,
Some("Cannot inline: expression contains assignment".to_string()),
);
}
(true, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_word_at_offset() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 10,
};
let inline = InlineRefactoring::new(&db, uri, position);
let source = "let x = 42";
let word = inline.extract_word_at_offset(source, 4).unwrap();
assert_eq!(word, "x");
}
#[test]
fn test_find_definition() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 0,
};
let inline = InlineRefactoring::new(&db, uri, position);
let source = "let x = 42\nlet y = x + 1";
let (range, value) = inline.find_definition(source, "x").unwrap();
assert_eq!(value, "42");
assert_eq!(range.start.line, 0);
}
#[test]
fn test_find_usages() {
let db = WindjammerDatabase::new();
let uri = Url::parse("file:///test.wj").unwrap();
let position = Position {
line: 0,
character: 0,
};
let inline = InlineRefactoring::new(&db, uri, position);
let source = "let x = 42\nlet y = x + 1\nlet z = x * 2";
let def_range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 1,
character: 0,
},
};
let usages = inline.find_usages(source, "x", def_range);
assert_eq!(usages.len(), 2);
}
}