1use std::collections::{HashMap, HashSet};
9use std::ops::{Range, RangeInclusive};
10
11use crate::model::{DiffModel, LineKind};
12use crate::syntax::registry::REGISTRY;
13use crate::syntax::{Ident, ScopeIndex};
14
15pub const MAX_SYMBOLS: usize = 9;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct LensOrigin {
21 pub path: String,
22 pub on_old_side: bool,
23 pub line: u32,
24}
25
26#[derive(Debug, Clone)]
30pub struct LensFile {
31 pub path: String,
32 pub old_text: Option<String>,
33 pub new_text: Option<String>,
34 pub old_lines: HashSet<u32>,
35 pub new_lines: HashSet<u32>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Reach {
41 Function(String),
43 File,
45 Diff,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct LensSymbol {
52 pub name: String,
53 pub reach: Reach,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct LensUse {
59 pub symbol: usize,
60 pub path: String,
61 pub on_old_side: bool,
62 pub line: u32,
63 pub range: Range<usize>,
64}
65
66struct Side {
68 idents: Vec<Ident>,
69 scope: ScopeIndex,
70}
71
72impl Side {
73 fn read(path: &str, text: Option<&str>) -> Option<Self> {
74 let text = text?;
75 let (idents, scope) = REGISTRY.symbols(path, text);
76 Some(Self { idents, scope })
77 }
78}
79
80pub fn compute(origin: &LensOrigin, files: &[LensFile]) -> LensData {
83 let sides: Vec<(Option<Side>, Option<Side>)> = files
84 .iter()
85 .map(|file| {
86 (
87 Side::read(&file.path, file.old_text.as_deref()),
88 Side::read(&file.path, file.new_text.as_deref()),
89 )
90 })
91 .collect();
92 let origin_side = files
93 .iter()
94 .position(|file| file.path == origin.path)
95 .and_then(|at| sides.get(at))
96 .and_then(|(old, new)| {
97 if origin.on_old_side {
98 old.as_ref()
99 } else {
100 new.as_ref()
101 }
102 });
103 let symbols = line_symbols(origin, origin_side);
104 let origin_span = origin_side
105 .and_then(|side| side.scope.enclosing(origin.line.saturating_sub(1) as usize))
106 .map(|(_, start, end)| start..=end);
107 let uses = symbol_uses(origin, origin_span.as_ref(), files, &sides, &symbols);
108 LensData {
109 origin: origin.clone(),
110 symbols,
111 uses,
112 }
113}
114
115fn line_symbols(origin: &LensOrigin, origin_side: Option<&Side>) -> Vec<LensSymbol> {
118 let origin_row = origin.line.saturating_sub(1) as usize;
119 let mut names: Vec<String> = Vec::new();
120 let mut items: HashSet<&str> = HashSet::new();
121 for ident in origin_side.map_or(&[][..], |side| side.idents.as_slice()) {
122 if ident.line != origin_row {
123 continue;
124 }
125 if ident.item {
126 items.insert(ident.name.as_str());
127 }
128 if !names.contains(&ident.name) && names.len() < MAX_SYMBOLS {
129 names.push(ident.name.clone());
130 }
131 }
132 let enclosing = origin_side.and_then(|side| side.scope.enclosing(origin_row));
133 names
134 .into_iter()
135 .map(|name| {
136 let reach = if items.contains(name.as_str()) {
139 Reach::Diff
140 } else if let Some((function, _, _)) = enclosing {
141 Reach::Function(function.to_owned())
142 } else {
143 Reach::File
144 };
145 LensSymbol { name, reach }
146 })
147 .collect()
148}
149
150fn symbol_uses(
152 origin: &LensOrigin,
153 origin_span: Option<&RangeInclusive<usize>>,
154 files: &[LensFile],
155 sides: &[(Option<Side>, Option<Side>)],
156 symbols: &[LensSymbol],
157) -> Vec<LensUse> {
158 let index: HashMap<&str, usize> = symbols
159 .iter()
160 .enumerate()
161 .map(|(at, symbol)| (symbol.name.as_str(), at))
162 .collect();
163 let mut uses = Vec::new();
164 for (file, (old, new)) in files.iter().zip(sides) {
165 for (on_old_side, side, shown) in
166 [(true, old, &file.old_lines), (false, new, &file.new_lines)]
167 {
168 let Some(side) = side else {
169 continue;
170 };
171 for ident in &side.idents {
172 let line = u32::try_from(ident.line + 1).unwrap_or(u32::MAX);
173 let Some(&symbol) = index.get(ident.name.as_str()) else {
174 continue;
175 };
176 if !shown.contains(&line) {
177 continue;
178 }
179 let reaches = match symbols.get(symbol).map(|s| &s.reach) {
180 Some(Reach::Diff) => true,
181 Some(Reach::File) => file.path == origin.path,
182 Some(Reach::Function(function)) if file.path == origin.path => {
183 let span = if on_old_side == origin.on_old_side {
186 origin_span.cloned()
187 } else {
188 side.scope
189 .def_span(function)
190 .map(|(start, end)| start..=end)
191 };
192 span.is_some_and(|span| span.contains(&ident.line))
193 }
194 Some(Reach::Function(_)) | None => false,
195 };
196 if reaches {
197 uses.push(LensUse {
198 symbol,
199 path: file.path.clone(),
200 on_old_side,
201 line,
202 range: ident.range.clone(),
203 });
204 }
205 }
206 }
207 }
208 uses
209}
210
211pub fn lens_files(model: &DiffModel) -> Vec<LensFile> {
213 model
214 .files
215 .iter()
216 .filter(|file| !file.binary)
217 .map(|file| {
218 let mut old_lines = HashSet::new();
219 let mut new_lines = HashSet::new();
220 for line in file.hunks.iter().flat_map(|hunk| &hunk.lines) {
221 match (line.kind, line.old_no, line.new_no) {
222 (LineKind::Deleted, Some(old), _) => {
223 old_lines.insert(old);
224 }
225 (LineKind::Added | LineKind::Context, _, Some(new)) => {
226 new_lines.insert(new);
227 }
228 _ => {}
229 }
230 }
231 LensFile {
232 path: file.path.clone(),
233 old_text: file.old_text.clone(),
234 new_text: file.new_text.clone(),
235 old_lines,
236 new_lines,
237 }
238 })
239 .collect()
240}
241
242#[derive(Debug, Clone)]
244pub struct LensData {
245 pub origin: LensOrigin,
246 pub symbols: Vec<LensSymbol>,
247 pub uses: Vec<LensUse>,
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
257 fn a_local_stays_in_the_function_its_line_sits_in() {
258 let text = "fn f() {\n let a = 1;\n}\n\nfn f() {\n let a = 2;\n a\n}\n";
259 let file = LensFile {
260 path: "a.rs".to_owned(),
261 old_text: None,
262 new_text: Some(text.to_owned()),
263 old_lines: HashSet::new(),
264 new_lines: (1..=8).collect(),
265 };
266 let origin = LensOrigin {
267 path: "a.rs".to_owned(),
268 on_old_side: false,
269 line: 6,
270 };
271 let lens = compute(&origin, &[file]);
272 let a = lens
273 .symbols
274 .iter()
275 .position(|symbol| symbol.name == "a")
276 .expect("a is named on the line");
277 let lines: Vec<u32> = lens
278 .uses
279 .iter()
280 .filter(|found| found.symbol == a)
281 .map(|found| found.line)
282 .collect();
283 assert_eq!(lines, [6, 7]);
284 }
285}