solidity_language_server/
rename.rs1use crate::goto;
2use crate::goto::CachedBuild;
3use crate::references;
4use crate::types::SourceLoc;
5use serde_json::Value;
6use std::collections::HashMap;
7use tower_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
8
9fn find_identifier_on_line(source_bytes: &[u8], line: u32, identifier: &str) -> Option<Range> {
13 let text = String::from_utf8_lossy(source_bytes);
14 let target_line = text.lines().nth(line as usize)?;
15 let ident_bytes = identifier.as_bytes();
18 let mut search_start = 0;
19 while let Some(offset) = target_line[search_start..].find(identifier) {
20 let col = search_start + offset;
21 let before_ok = col == 0 || {
22 let b = target_line.as_bytes()[col - 1];
23 !b.is_ascii_alphanumeric() && b != b'_'
24 };
25 let after_ok = col + ident_bytes.len() >= target_line.len() || {
26 let b = target_line.as_bytes()[col + ident_bytes.len()];
27 !b.is_ascii_alphanumeric() && b != b'_'
28 };
29 if before_ok && after_ok {
30 let line_start_byte: usize = text
32 .lines()
33 .take(line as usize)
34 .map(|l| l.len() + 1) .sum();
36 let start = crate::utils::byte_offset_to_position(&text, line_start_byte + col);
37 let end = crate::utils::byte_offset_to_position(
38 &text,
39 line_start_byte + col + ident_bytes.len(),
40 );
41 return Some(Range { start, end });
42 }
43 search_start = col + 1;
44 }
45 None
46}
47
48fn get_text_at_range(source_bytes: &[u8], range: &Range) -> Option<String> {
49 let start_byte = goto::pos_to_bytes(source_bytes, range.start);
50 let end_byte = goto::pos_to_bytes(source_bytes, range.end);
51 if end_byte > source_bytes.len() {
52 return None;
53 }
54 String::from_utf8(source_bytes[start_byte..end_byte].to_vec()).ok()
55}
56
57fn get_name_location_index(
58 ast_data: &Value,
59 file_uri: &Url,
60 position: Position,
61 source_bytes: &[u8],
62) -> Option<usize> {
63 let sources = ast_data.get("sources")?;
64 let (nodes, path_to_abs, _external_refs) = goto::cache_ids(sources);
65 let path = file_uri.to_file_path().ok()?;
66 let path_str = path.to_str()?;
67 let abs_path = path_to_abs.get(path_str)?;
68 let byte_position = goto::pos_to_bytes(source_bytes, position);
69 let node_id = references::byte_to_id(&nodes, abs_path, byte_position)?;
70 let file_nodes = nodes.get(abs_path)?;
71 let node_info = file_nodes.get(&node_id)?;
72
73 if !node_info.name_locations.is_empty() {
74 for (i, name_loc) in node_info.name_locations.iter().enumerate() {
75 if let Some(loc) = SourceLoc::parse(name_loc)
76 && loc.offset <= byte_position
77 && byte_position < loc.end()
78 {
79 return Some(i);
80 }
81 }
82 }
83 None
84}
85
86pub fn get_identifier_at_position(source_bytes: &[u8], position: Position) -> Option<String> {
87 let text = String::from_utf8_lossy(source_bytes);
88 let abs_offset = crate::utils::position_to_byte_offset(&text, position);
89 let lines: Vec<&str> = text.lines().collect();
90 let line = lines.get(position.line as usize)?;
91 let line_start = text
93 .as_bytes()
94 .iter()
95 .take(abs_offset)
96 .enumerate()
97 .rev()
98 .find(|&(_, &b)| b == b'\n')
99 .map(|(i, _)| i + 1)
100 .unwrap_or(0);
101 let col_byte = abs_offset - line_start;
102 if col_byte > line.len() {
103 return None;
104 }
105 let mut start = col_byte;
106 let mut end = col_byte;
107
108 while start > 0
109 && (line.as_bytes()[start - 1].is_ascii_alphanumeric()
110 || line.as_bytes()[start - 1] == b'_')
111 {
112 start -= 1;
113 }
114 while end < line.len()
115 && (line.as_bytes()[end].is_ascii_alphanumeric() || line.as_bytes()[end] == b'_')
116 {
117 end += 1;
118 }
119
120 if start == end {
121 return None;
122 }
123 if line.as_bytes()[start].is_ascii_digit() {
124 return None;
125 }
126
127 Some(line[start..end].to_string())
128}
129
130pub fn get_identifier_range(source_bytes: &[u8], position: Position) -> Option<Range> {
131 let text = String::from_utf8_lossy(source_bytes);
132 let abs_offset = crate::utils::position_to_byte_offset(&text, position);
133 let lines: Vec<&str> = text.lines().collect();
134 let line = lines.get(position.line as usize)?;
135 let line_start = text
137 .as_bytes()
138 .iter()
139 .take(abs_offset)
140 .enumerate()
141 .rev()
142 .find(|&(_, &b)| b == b'\n')
143 .map(|(i, _)| i + 1)
144 .unwrap_or(0);
145 let col_byte = abs_offset - line_start;
146 if col_byte > line.len() {
147 return None;
148 }
149 let mut start = col_byte;
150 let mut end = col_byte;
151
152 while start > 0
153 && (line.as_bytes()[start - 1].is_ascii_alphanumeric()
154 || line.as_bytes()[start - 1] == b'_')
155 {
156 start -= 1;
157 }
158 while end < line.len()
159 && (line.as_bytes()[end].is_ascii_alphanumeric() || line.as_bytes()[end] == b'_')
160 {
161 end += 1;
162 }
163
164 if start == end {
165 return None;
166 }
167 if line.as_bytes()[start].is_ascii_digit() {
168 return None;
169 }
170
171 let start = crate::utils::byte_offset_to_position(&text, line_start + start);
173 let end = crate::utils::byte_offset_to_position(&text, line_start + end);
174
175 Some(Range { start, end })
176}
177
178type Type = HashMap<Url, HashMap<(u32, u32, u32, u32), TextEdit>>;
179
180pub fn rename_symbol(
181 build: &CachedBuild,
182 file_uri: &Url,
183 position: Position,
184 source_bytes: &[u8],
185 new_name: String,
186 other_builds: &[&CachedBuild],
187 text_buffers: &HashMap<String, Vec<u8>>,
188) -> Option<WorkspaceEdit> {
189 let original_identifier = get_identifier_at_position(source_bytes, position)?;
190 let name_location_index = get_name_location_index(&build.ast, file_uri, position, source_bytes);
191 let mut locations = references::goto_references_with_index(
192 &build.ast,
193 file_uri,
194 position,
195 source_bytes,
196 name_location_index,
197 true, );
199
200 if let Some((def_abs_path, def_byte_offset)) =
202 references::resolve_target_location(build, file_uri, position, source_bytes)
203 {
204 for other_build in other_builds {
205 let other_locations = references::goto_references_for_target(
206 other_build,
207 &def_abs_path,
208 def_byte_offset,
209 name_location_index,
210 true, );
212 locations.extend(other_locations);
213 }
214 }
215
216 let mut seen = std::collections::HashSet::new();
218 locations.retain(|loc| {
219 seen.insert((
220 loc.uri.clone(),
221 loc.range.start.line,
222 loc.range.start.character,
223 loc.range.end.line,
224 loc.range.end.character,
225 ))
226 });
227
228 if locations.is_empty() {
229 return None;
230 }
231 let mut changes: Type = HashMap::new();
232 for location in locations {
233 let file_source_bytes = if let Some(buf) = text_buffers.get(location.uri.as_str()) {
236 buf.clone()
237 } else {
238 let absolute_path = match location.uri.to_file_path() {
239 Ok(p) => p,
240 Err(_) => continue,
241 };
242 match std::fs::read(&absolute_path) {
243 Ok(b) => b,
244 Err(_) => continue,
245 }
246 };
247 let text_at_range = get_text_at_range(&file_source_bytes, &location.range);
248 let actual_range = if text_at_range.as_deref() == Some(&original_identifier) {
249 location.range
251 } else {
252 match find_identifier_on_line(
255 &file_source_bytes,
256 location.range.start.line,
257 &original_identifier,
258 ) {
259 Some(corrected) => corrected,
260 None => continue,
261 }
262 };
263 let text_edit = TextEdit {
264 range: actual_range,
265 new_text: new_name.clone(),
266 };
267 let key = (
268 actual_range.start.line,
269 actual_range.start.character,
270 actual_range.end.line,
271 actual_range.end.character,
272 );
273 changes
274 .entry(location.uri)
275 .or_default()
276 .insert(key, text_edit);
277 }
278 let changes_vec: HashMap<Url, Vec<TextEdit>> = changes
279 .into_iter()
280 .map(|(uri, edits_map)| (uri, edits_map.into_values().collect()))
281 .collect();
282 Some(WorkspaceEdit {
283 changes: Some(changes_vec),
284 document_changes: None,
285 change_annotations: None,
286 })
287}