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