php_lsp/actions/
inline_action.rs1use std::collections::HashMap;
10
11use tower_lsp_server::ls_types::{
12 CodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit, Uri, WorkspaceEdit,
13};
14
15use crate::text::word_at_position;
16
17pub fn inline_variable_actions(source: &str, range: Range, uri: &Uri) -> Vec<CodeActionOrCommand> {
18 let cursor = range.start;
20 let var_name = match word_at_position(source, cursor) {
21 Some(w) if w.starts_with('$') => w,
22 _ => return vec![],
23 };
24
25 let (assign_line_no, rhs) = match find_unique_assignment(source, &var_name, cursor.line) {
29 Some(v) => v,
30 None => return vec![],
31 };
32
33 let usages = collect_usages(source, &var_name, assign_line_no + 1);
35 if usages.is_empty() {
36 return vec![];
37 }
38
39 let mut edits: Vec<TextEdit> = usages
41 .into_iter()
42 .map(|usage_range| TextEdit {
43 range: usage_range,
44 new_text: rhs.clone(),
45 })
46 .collect();
47
48 edits.push(TextEdit {
50 range: Range {
51 start: Position {
52 line: assign_line_no,
53 character: 0,
54 },
55 end: Position {
56 line: assign_line_no + 1,
57 character: 0,
58 },
59 },
60 new_text: String::new(),
61 });
62
63 let mut changes = HashMap::new();
64 changes.insert(uri.clone(), edits);
65
66 vec![CodeActionOrCommand::CodeAction(CodeAction {
67 title: format!("Inline variable '{var_name}'"),
68 kind: Some(CodeActionKind::REFACTOR_INLINE),
69 edit: Some(WorkspaceEdit {
70 changes: Some(changes),
71 ..Default::default()
72 }),
73 ..Default::default()
74 })]
75}
76
77fn find_unique_assignment(source: &str, var_name: &str, before_line: u32) -> Option<(u32, String)> {
83 let lines: Vec<&str> = source.lines().collect();
84 let mut hit: Option<(u32, String)> = None;
85
86 for (i, line) in lines.iter().enumerate() {
87 let trimmed = line.trim();
88 let prefix = format!("{var_name} =");
89 let Some(rest) = trimmed.strip_prefix(prefix.as_str()) else {
90 continue;
91 };
92 if rest.starts_with('=') {
94 continue;
95 }
96 let Some((rhs_part, trailing)) = split_statement(rest) else {
100 continue;
101 };
102 let rhs = rhs_part.trim();
103 if rhs.is_empty() {
104 continue;
105 }
106 if !trailing.trim().is_empty() {
109 continue;
110 }
111 if hit.is_some() {
112 return None; }
114 hit = Some((i as u32, rhs.to_string()));
115 }
116
117 hit.filter(|(line_no, _)| *line_no < before_line)
120}
121
122fn split_statement(rest: &str) -> Option<(&str, &str)> {
127 let bytes = rest.as_bytes();
128 let mut depth: i32 = 0;
129 let mut i = 0usize;
130 while i < bytes.len() {
131 match bytes[i] {
132 b'(' | b'[' | b'{' => depth += 1,
133 b')' | b']' | b'}' => depth -= 1,
134 b'\'' | b'"' => {
135 let quote = bytes[i];
136 i += 1;
137 while i < bytes.len() {
138 if bytes[i] == b'\\' {
139 i += 2;
140 continue;
141 }
142 if bytes[i] == quote {
143 break;
144 }
145 i += 1;
146 }
147 }
148 b';' if depth == 0 => return Some((&rest[..i], &rest[i + 1..])),
149 _ => {}
150 }
151 i += 1;
152 }
153 None
154}
155
156fn collect_usages(source: &str, var_name: &str, from_line: u32) -> Vec<Range> {
159 let mut usages = Vec::new();
160 for (line_idx, line) in source.lines().enumerate() {
161 if (line_idx as u32) < from_line {
162 continue;
163 }
164 let mut search_from = 0usize;
165 while let Some(pos) = line[search_from..].find(var_name) {
166 let abs = search_from + pos;
167 let before_ok = abs == 0
169 || line
170 .as_bytes()
171 .get(abs - 1)
172 .is_none_or(|b| !b.is_ascii_alphanumeric() && *b != b'_');
173 let after_ok = line
175 .as_bytes()
176 .get(abs + var_name.len())
177 .is_none_or(|b| !b.is_ascii_alphanumeric() && *b != b'_');
178
179 if before_ok && after_ok {
180 let after_var = line[abs + var_name.len()..].trim_start();
182 if after_var.starts_with('=') && !after_var.starts_with("==") {
183 search_from = abs + var_name.len();
184 continue;
185 }
186
187 let char_start = byte_col_to_utf16_col(line, abs);
188 let char_end = byte_col_to_utf16_col(line, abs + var_name.len());
189 usages.push(Range {
190 start: Position {
191 line: line_idx as u32,
192 character: char_start as u32,
193 },
194 end: Position {
195 line: line_idx as u32,
196 character: char_end as u32,
197 },
198 });
199 }
200 search_from = abs + 1;
201 }
202 }
203 usages
204}
205
206fn byte_col_to_utf16_col(line: &str, byte_col: usize) -> usize {
207 line[..byte_col.min(line.len())]
208 .chars()
209 .map(|c| c.len_utf16())
210 .sum()
211}