Skip to main content

solidity_language_server/
rename.rs

1use 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
9/// Search a specific line for an identifier and return its exact range.
10/// Used to correct stale AST ranges when the buffer has been edited but
11/// not saved (e.g. after a previous rename).
12fn 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    // Find all occurrences of the identifier on this line, bounded by
16    // non-identifier characters so we don't match substrings.
17    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            // Compute encoding-aware column positions
31            let line_start_byte: usize = text
32                .lines()
33                .take(line as usize)
34                .map(|l| l.len() + 1) // +1 for newline
35                .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    // Compute byte offset within this line
92    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    // Compute byte offset of line start and cursor column within line
136    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    // Convert byte offsets back to encoding-aware positions
172    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, // rename always includes the declaration
198    );
199
200    // Cross-file: scan other cached ASTs for the same target definition
201    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, // rename always includes the declaration
211            );
212            locations.extend(other_locations);
213        }
214    }
215
216    // Deduplicate
217    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        // Read the file content, preferring in-memory text buffers (which
234        // reflect unsaved editor changes) over reading from disk.
235        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            // AST range matches the buffer — use it directly
250            location.range
251        } else {
252            // AST range is stale (e.g. buffer was edited but not saved).
253            // Search the same line for the identifier and correct the range.
254            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}