1use code_moniker_core::core::code_graph::RefRecord;
2use code_moniker_core::core::kinds::BIND_IMPORT;
3use code_moniker_core::core::moniker::Moniker;
4use rustc_hash::FxHashMap;
5
6use crate::code::ref_kind;
7use crate::lines::LineIndex;
8
9use super::model::{HunkCoverage, RefChange, RefChangeKind, SymbolChange};
10use super::pairing::FileSide;
11
12pub struct RenameContext {
13 pairs: Vec<(Moniker, Moniker)>,
14}
15
16impl RenameContext {
17 pub fn from_changes(changes: &[SymbolChange]) -> Self {
18 let pairs = changes
19 .iter()
20 .filter_map(|change| {
21 let old = change.old.as_ref()?.moniker.clone();
22 let new = change.new.as_ref()?.moniker.clone();
23 (old != new).then_some((old, new))
24 })
25 .collect();
26 Self { pairs }
27 }
28
29 pub fn push_pair(&mut self, old: Moniker, new: Moniker) {
30 if old != new {
31 self.pairs.push((old, new));
32 }
33 }
34
35 fn apply(&self, target: &Moniker) -> Option<Moniker> {
36 let view = target.as_view();
37 let mut best: Option<&(Moniker, Moniker)> = None;
38 for pair in &self.pairs {
39 if !pair.0.as_view().is_ancestor_of(&view) {
40 continue;
41 }
42 if best.is_none_or(|kept| pair.0.as_encoded().len() > kept.0.as_encoded().len()) {
43 best = Some(pair);
44 }
45 }
46 let (from, to) = best?;
47 let mut bytes = to.as_encoded().to_vec();
48 bytes.extend_from_slice(&target.as_encoded()[from.as_encoded().len()..]);
49 Moniker::from_encoded(bytes).ok()
50 }
51}
52
53type RefKey = (Vec<u8>, Vec<u8>, Vec<u8>, Option<usize>, Vec<u8>, Vec<u8>);
54
55struct RefFact {
56 raw_key: RefKey,
57 mapped_key: Option<RefKey>,
58 ref_kind: String,
59 import: bool,
60 target: Moniker,
61 line_range: Option<(u32, u32)>,
62}
63
64pub fn pair_refs(
65 base: &FileSide<'_>,
66 current: &FileSide<'_>,
67 ctx: &RenameContext,
68) -> Vec<RefChange> {
69 let old_span = tracing::info_span!(
70 "workspace.change_overlay.collect_base_references",
71 file.path = %base.file_path.display(),
72 graph.references = base.graph.ref_count(),
73 );
74 let mut old_facts = old_span.in_scope(|| collect_ref_facts(base, Some(ctx)));
75 let new_span = tracing::info_span!(
76 "workspace.change_overlay.collect_current_references",
77 file.path = %current.file_path.display(),
78 graph.references = current.graph.ref_count(),
79 );
80 let mut new_facts = new_span.in_scope(|| collect_ref_facts(current, None));
81 let cancel_span = tracing::info_span!(
82 "workspace.change_overlay.cancel_unchanged_references",
83 file.path = %current.file_path.display(),
84 );
85 cancel_span.in_scope(|| cancel_unchanged(&mut old_facts, &mut new_facts));
86 let retarget_span = tracing::info_span!(
87 "workspace.change_overlay.pair_retargeted_references",
88 file.path = %current.file_path.display(),
89 );
90 let mut changes =
91 retarget_span.in_scope(|| pair_retargets(&mut old_facts, &mut new_facts, current));
92 let materialize_span = tracing::info_span!(
93 "workspace.change_overlay.materialize_reference_changes",
94 file.path = %current.file_path.display(),
95 );
96 materialize_span.in_scope(|| {
97 changes.extend(old_facts.into_iter().flatten().map(|fact| RefChange {
98 kind: RefChangeKind::Removed,
99 file_path: base.file_path.to_path_buf(),
100 ref_kind: fact.ref_kind,
101 old_target: Some(fact.target),
102 new_target: None,
103 old_line_range: fact.line_range,
104 new_line_range: None,
105 }));
106 changes.extend(new_facts.into_iter().flatten().map(|fact| RefChange {
107 kind: RefChangeKind::Added,
108 file_path: current.file_path.to_path_buf(),
109 ref_kind: fact.ref_kind,
110 old_target: None,
111 new_target: Some(fact.target),
112 old_line_range: None,
113 new_line_range: fact.line_range,
114 }));
115 });
116 changes
117}
118
119fn collect_ref_facts(file: &FileSide<'_>, ctx: Option<&RenameContext>) -> Vec<Option<RefFact>> {
120 let lines = LineIndex::new(file.source);
121 file.graph
122 .refs()
123 .map(|record| Some(ref_fact(file, record, &lines, ctx)))
124 .collect()
125}
126
127fn ref_fact(
128 file: &FileSide<'_>,
129 record: &RefRecord,
130 lines: &LineIndex,
131 ctx: Option<&RenameContext>,
132) -> RefFact {
133 let source = file.graph.def_at(record.source).moniker.clone();
134 let raw_key = ref_key(record, &source, &record.target);
135 let mapped_key = ctx.and_then(|ctx| {
136 let mapped_source = ctx.apply(&source);
137 let mapped_target = ctx.apply(&record.target);
138 if mapped_source.is_none() && mapped_target.is_none() {
139 return None;
140 }
141 Some(ref_key(
142 record,
143 mapped_source.as_ref().unwrap_or(&source),
144 mapped_target.as_ref().unwrap_or(&record.target),
145 ))
146 });
147 RefFact {
148 raw_key,
149 mapped_key,
150 ref_kind: ref_kind(record),
151 import: record.binding.as_ref() == BIND_IMPORT,
152 target: record.target.clone(),
153 line_range: record
154 .position
155 .map(|(start, end)| lines.line_range(start, end)),
156 }
157}
158
159fn ref_key(record: &RefRecord, source: &Moniker, target: &Moniker) -> RefKey {
160 (
161 source.as_encoded().to_vec(),
162 target.as_encoded().to_vec(),
163 record.kind.to_vec(),
164 record.call_arity,
165 record.alias.to_vec(),
166 record.binding.to_vec(),
167 )
168}
169
170fn cancel_unchanged(old_facts: &mut [Option<RefFact>], new_facts: &mut [Option<RefFact>]) {
171 let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
172 for (idx, fact) in new_facts.iter().enumerate() {
173 if let Some(fact) = fact {
174 by_key.entry(fact.raw_key.clone()).or_default().push(idx);
175 }
176 }
177 for old_slot in old_facts.iter_mut() {
178 let Some(fact) = old_slot else { continue };
179 let Some(matches) = by_key.get_mut(&fact.raw_key) else {
180 continue;
181 };
182 let Some(new_idx) = matches.pop() else {
183 continue;
184 };
185 new_facts[new_idx] = None;
186 *old_slot = None;
187 }
188}
189
190fn pair_retargets(
191 old_facts: &mut [Option<RefFact>],
192 new_facts: &mut [Option<RefFact>],
193 current: &FileSide<'_>,
194) -> Vec<RefChange> {
195 let mut by_key: FxHashMap<RefKey, Vec<usize>> = FxHashMap::default();
196 for (idx, fact) in new_facts.iter().enumerate() {
197 if let Some(fact) = fact {
198 by_key.entry(fact.raw_key.clone()).or_default().push(idx);
199 }
200 }
201 let mut changes = Vec::new();
202 for old_slot in old_facts.iter_mut() {
203 let Some(fact) = old_slot else { continue };
204 let Some(mapped_key) = fact.mapped_key.as_ref() else {
205 continue;
206 };
207 let Some(new_idx) = by_key.get_mut(mapped_key).and_then(Vec::pop) else {
208 continue;
209 };
210 let old = old_slot.take().expect("checked above");
211 let new = new_facts[new_idx].take().expect("indexed above");
212 let kind = if old.import {
213 RefChangeKind::ImportRetargeted
214 } else {
215 RefChangeKind::CallSiteRetargeted
216 };
217 changes.push(RefChange {
218 kind,
219 file_path: current.file_path.to_path_buf(),
220 ref_kind: new.ref_kind,
221 old_target: Some(old.target),
222 new_target: Some(new.target),
223 old_line_range: old.line_range,
224 new_line_range: new.line_range,
225 });
226 }
227 changes
228}
229
230pub struct CoverageInputs<'a> {
231 pub old_hunks: &'a [(u32, u32)],
232 pub new_hunks: &'a [(u32, u32)],
233 pub old_explained: &'a [(u32, u32)],
234 pub new_explained: &'a [(u32, u32)],
235}
236
237pub fn hunk_coverage(inputs: CoverageInputs<'_>) -> HunkCoverage {
238 HunkCoverage {
239 old_residual: residual_spans(inputs.old_hunks, inputs.old_explained),
240 new_residual: residual_spans(inputs.new_hunks, inputs.new_explained),
241 }
242}
243
244fn residual_spans(hunks: &[(u32, u32)], explained: &[(u32, u32)]) -> Vec<(u32, u32)> {
245 let covered = merged_spans(explained);
246 let mut out = Vec::new();
247 for &(start, end) in hunks {
248 let mut cursor = start;
249 for &(covered_start, covered_end) in &covered {
250 if covered_end < cursor || covered_start > end {
251 continue;
252 }
253 if covered_start > cursor {
254 out.push((cursor, covered_start - 1));
255 }
256 cursor = cursor.max(covered_end.saturating_add(1));
257 if cursor > end {
258 break;
259 }
260 }
261 if cursor <= end {
262 out.push((cursor, end));
263 }
264 }
265 out
266}
267
268fn merged_spans(spans: &[(u32, u32)]) -> Vec<(u32, u32)> {
269 let mut sorted = spans.to_vec();
270 sorted.sort_unstable();
271 let mut merged: Vec<(u32, u32)> = Vec::new();
272 for (start, end) in sorted {
273 match merged.last_mut() {
274 Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
275 _ => merged.push((start, end)),
276 }
277 }
278 merged
279}
280
281#[cfg(test)]
282mod tests {
283 use super::super::pairing::{PairInputs, finish_files, pair_file};
284 use super::*;
285 use crate::environment;
286 use code_moniker_core::core::moniker::MonikerBuilder;
287 use code_moniker_core::lang::Lang;
288 use std::path::Path;
289
290 struct Extraction {
291 graph: code_moniker_core::core::code_graph::CodeGraph,
292 source: String,
293 rel: String,
294 }
295
296 fn extract(source: &str, rel: &str) -> Extraction {
297 Extraction {
298 graph: environment::extract_source(Lang::Rs, source, Path::new(rel)),
299 source: source.to_string(),
300 rel: rel.to_string(),
301 }
302 }
303
304 fn file_side(extraction: &Extraction) -> FileSide<'_> {
305 FileSide {
306 lang: Lang::Rs,
307 graph: &extraction.graph,
308 source: &extraction.source,
309 file_path: Path::new(&extraction.rel),
310 }
311 }
312
313 fn moniker(segments: &[(&[u8], &[u8])]) -> Moniker {
314 let mut builder = MonikerBuilder::new();
315 builder.project(b"app");
316 for (kind, name) in segments {
317 builder.segment(kind, name);
318 }
319 builder.build()
320 }
321
322 #[test]
323 fn rename_context_uses_the_longest_indexed_ancestor() {
324 let old_module = moniker(&[(b"module", b"old")]);
325 let new_module = moniker(&[(b"module", b"new")]);
326 let old_function = moniker(&[(b"module", b"old"), (b"fn", b"work()")]);
327 let new_function = moniker(&[(b"module", b"new"), (b"fn", b"run()")]);
328 let target = moniker(&[
329 (b"module", b"old"),
330 (b"fn", b"work()"),
331 (b"local", b"value"),
332 ]);
333 let expected = moniker(&[(b"module", b"new"), (b"fn", b"run()"), (b"local", b"value")]);
334 let mut context = RenameContext::from_changes(&[]);
335 context.push_pair(old_module, new_module);
336 context.push_pair(old_function, new_function);
337
338 assert_eq!(context.apply(&target), Some(expected));
339 }
340
341 #[test]
342 fn call_sites_retarget_after_a_rename() {
343 let base = extract(
344 "fn helper(x: u32) -> u32 { x }\nfn caller() { helper(1); helper(2); }\n",
345 "src/lib.rs",
346 );
347 let current = extract(
348 "fn assist(x: u32) -> u32 { x }\nfn caller() { assist(1); assist(2); }\n",
349 "src/lib.rs",
350 );
351 let symbol_changes = finish_files(vec![pair_file(PairInputs {
352 base: file_side(&base),
353 current: file_side(¤t),
354 file_moved: false,
355 })]);
356 let ctx = RenameContext::from_changes(&symbol_changes);
357
358 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
359
360 let retargeted_calls: Vec<_> = ref_changes
361 .iter()
362 .filter(|change| {
363 change.kind == RefChangeKind::CallSiteRetargeted && change.ref_kind == "calls"
364 })
365 .collect();
366 assert_eq!(retargeted_calls.len(), 2, "{ref_changes:?}");
367 assert!(
368 ref_changes.iter().all(|change| change.kind.is_retarget()),
369 "no stray added/removed refs: {ref_changes:?}"
370 );
371 }
372
373 #[test]
374 fn imports_retarget_through_a_module_prefix_pair() {
375 let base = extract(
376 "mod helpers;\nuse crate::helpers::assist;\n\nfn caller() { assist(); }\n",
377 "src/lib.rs",
378 );
379 let current = extract(
380 "mod support;\nuse crate::support::assist;\n\nfn caller() { assist(); }\n",
381 "src/lib.rs",
382 );
383 let old_module = extract("pub fn assist() {}\n", "src/helpers.rs");
384 let new_module = extract("pub fn assist() {}\n", "src/support.rs");
385 let mut ctx = RenameContext::from_changes(&[]);
386 ctx.push_pair(
387 old_module.graph.root().clone(),
388 new_module.graph.root().clone(),
389 );
390
391 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
392
393 assert!(
394 ref_changes
395 .iter()
396 .any(|change| change.kind == RefChangeKind::ImportRetargeted
397 && change.ref_kind == "imports_symbol"),
398 "symbol import must retarget through the module prefix: {ref_changes:?}"
399 );
400 assert!(
401 ref_changes.iter().all(|change| change.kind.is_retarget()),
402 "{ref_changes:?}"
403 );
404 }
405
406 #[test]
407 fn unrelated_ref_edits_stay_added_and_removed() {
408 let base = extract("fn caller() { alpha(); }\n", "src/lib.rs");
409 let current = extract("fn caller() { beta(); }\n", "src/lib.rs");
410 let ctx = RenameContext::from_changes(&[]);
411
412 let ref_changes = pair_refs(&file_side(&base), &file_side(¤t), &ctx);
413
414 let labels: Vec<_> = ref_changes.iter().map(|change| change.kind).collect();
415 assert!(labels.contains(&RefChangeKind::Added), "{ref_changes:?}");
416 assert!(labels.contains(&RefChangeKind::Removed), "{ref_changes:?}");
417 }
418
419 #[test]
420 fn coverage_subtracts_explained_spans() {
421 let coverage = hunk_coverage(CoverageInputs {
422 old_hunks: &[],
423 new_hunks: &[(10, 20), (30, 31)],
424 old_explained: &[],
425 new_explained: &[(9, 15), (18, 20)],
426 });
427
428 assert_eq!(coverage.new_residual, vec![(16, 17), (30, 31)]);
429 assert!(!coverage.explained());
430 }
431
432 #[test]
433 fn coverage_is_explained_when_all_hunks_are_covered() {
434 let coverage = hunk_coverage(CoverageInputs {
435 old_hunks: &[(5, 6)],
436 new_hunks: &[(10, 20)],
437 old_explained: &[(1, 8)],
438 new_explained: &[(10, 14), (15, 20)],
439 });
440
441 assert!(coverage.explained(), "{coverage:?}");
442 }
443}