Skip to main content

diffler_core/
lens.rs

1//! The symbol lens's model: the names on one diff line, how far each one
2//! reaches, and every use of them on a line the diff shows. A local name
3//! reaches as far as its enclosing function; a function, method or type (a
4//! definition, a call or a type position) reaches across every file of the
5//! diff. The names come from the parse tree, so only the identifiers of the
6//! code count.
7
8use 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
15/// The most names one line's lens labels, one per digit key.
16pub const MAX_SYMBOLS: usize = 9;
17
18/// The diff line the lens was opened on.
19#[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/// One file of the diff, as the worker reads it: both sides' text and the
27/// line numbers the diff shows on each, a deleted line on the old side and an
28/// added or context line on the new one, so a context line counts once.
29#[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/// How far a symbol's uses are looked for.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Reach {
41    /// A local name, inside the function named here.
42    Function(String),
43    /// A name at the top level of its file, outside any function.
44    File,
45    /// A function, method or type: a definition, a call or a type position,
46    /// linked across every file of the diff.
47    Diff,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct LensSymbol {
52    pub name: String,
53    pub reach: Reach,
54}
55
56/// One use of a symbol on a line the diff shows.
57#[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
66/// One side of one file, parsed once for both its names and its scopes.
67struct 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
80/// Build the lens for `origin` over `files`. It parses both sides of every
81/// file, so it belongs on a worker thread.
82pub 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
115/// The names on the origin line, first appearance first, each with how far
116/// its uses are looked for.
117fn 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            // a local that only shares its name with some function elsewhere
137            // stays local: only a call, a definition or a type reaches out
138            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
150/// Every use of `symbols` on a line the diff shows, within each one's reach.
151fn 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                        // a file can define two functions of one name, so on the
184                        // origin's own side we hold to the one the line sits in
185                        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
211/// The files of `model` as [`compute`] reads them.
212pub 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/// A built lens: the line it was opened on, its names, and their uses.
243#[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    /// Two functions of one name in one file: a local of the second reaches
255    /// only the second.
256    #[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}