use std::collections::HashMap;
use tower_lsp::lsp_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit, Url, WorkspaceEdit,
};
use crate::text::word_at_position;
pub fn inline_variable_actions(source: &str, range: Range, uri: &Url) -> Vec<CodeActionOrCommand> {
let cursor = range.start;
let var_name = match word_at_position(source, cursor) {
Some(w) if w.starts_with('$') => w,
_ => return vec![],
};
let (assign_line_no, rhs) = match find_unique_assignment(source, &var_name, cursor.line) {
Some(v) => v,
None => return vec![],
};
let usages = collect_usages(source, &var_name, assign_line_no + 1);
if usages.is_empty() {
return vec![];
}
let mut edits: Vec<TextEdit> = usages
.into_iter()
.map(|usage_range| TextEdit {
range: usage_range,
new_text: rhs.clone(),
})
.collect();
edits.push(TextEdit {
range: Range {
start: Position {
line: assign_line_no,
character: 0,
},
end: Position {
line: assign_line_no + 1,
character: 0,
},
},
new_text: String::new(),
});
let mut changes = HashMap::new();
changes.insert(uri.clone(), edits);
vec![CodeActionOrCommand::CodeAction(CodeAction {
title: format!("Inline variable '{var_name}'"),
kind: Some(CodeActionKind::REFACTOR_INLINE),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}),
..Default::default()
})]
}
fn find_unique_assignment(source: &str, var_name: &str, before_line: u32) -> Option<(u32, String)> {
let lines: Vec<&str> = source.lines().collect();
let mut hit: Option<(u32, String)> = None;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
let prefix = format!("{var_name} =");
let Some(rest) = trimmed.strip_prefix(prefix.as_str()) else {
continue;
};
if rest.starts_with('=') {
continue;
}
let rhs = rest.trim().trim_end_matches(';').trim();
if rhs.is_empty() {
continue;
}
if hit.is_some() {
return None; }
hit = Some((i as u32, rhs.to_string()));
}
hit.filter(|(line_no, _)| *line_no < before_line)
}
fn collect_usages(source: &str, var_name: &str, from_line: u32) -> Vec<Range> {
let mut usages = Vec::new();
for (line_idx, line) in source.lines().enumerate() {
if (line_idx as u32) < from_line {
continue;
}
let mut search_from = 0usize;
while let Some(pos) = line[search_from..].find(var_name) {
let abs = search_from + pos;
let before_ok = abs == 0
|| line
.as_bytes()
.get(abs - 1)
.is_none_or(|b| !b.is_ascii_alphanumeric() && *b != b'_');
let after_ok = line
.as_bytes()
.get(abs + var_name.len())
.is_none_or(|b| !b.is_ascii_alphanumeric() && *b != b'_');
if before_ok && after_ok {
let after_var = line[abs + var_name.len()..].trim_start();
if after_var.starts_with('=') && !after_var.starts_with("==") {
search_from = abs + var_name.len();
continue;
}
let char_start = byte_col_to_utf16_col(line, abs);
let char_end = byte_col_to_utf16_col(line, abs + var_name.len());
usages.push(Range {
start: Position {
line: line_idx as u32,
character: char_start as u32,
},
end: Position {
line: line_idx as u32,
character: char_end as u32,
},
});
}
search_from = abs + 1;
}
}
usages
}
fn byte_col_to_utf16_col(line: &str, byte_col: usize) -> usize {
line[..byte_col.min(line.len())]
.chars()
.map(|c| c.len_utf16())
.sum()
}