Skip to main content

diffler_core/syntax/
mod.rs

1//! Tree-sitter foundation shared by highlighting, scope context, and the
2//! structural diff. Grammars are statically linked (no runtime loading, since
3//! musl-static binaries cannot `dlopen`); a parse failure or unknown language
4//! degrades silently to plain behavior so the UI is never blocked.
5
6pub mod intraline;
7pub mod registry;
8pub mod scope;
9pub mod symbols;
10
11use std::ops::Range;
12
13use tree_sitter::Parser;
14
15pub use registry::{HIGHLIGHT_NAMES, LangEntry, LanguageRegistry};
16pub use scope::ScopeIndex;
17pub use symbols::Ident;
18
19/// Files larger than this are not parsed (avoids pathological cost on
20/// generated/minified blobs); they degrade to plain rendering / textual diff.
21pub(crate) const MAX_PARSE_BYTES: usize = 2_000_000;
22
23/// Parse `src` with `entry`'s grammar. `None` on a language-setup or parse
24/// failure so callers degrade gracefully.
25pub(crate) fn parse(entry: &LangEntry, src: &str) -> Option<tree_sitter::Tree> {
26    let mut parser = Parser::new();
27    parser.set_language(&entry.language).ok()?;
28    parser.parse(src, None)
29}
30
31/// `(start_byte, visible_end_byte)` per line, matching `str::lines()`: the
32/// visible end excludes the trailing `\n`/`\r\n`. Shared by highlighting and
33/// intra-line emphasis to map whole-file byte ranges onto individual lines.
34pub(crate) fn line_bounds(content: &str) -> Vec<(usize, usize)> {
35    let bytes = content.as_bytes();
36    let mut out = Vec::new();
37    let mut start = 0;
38    for (i, &b) in bytes.iter().enumerate() {
39        if b == b'\n' {
40            let end = if i > start && bytes.get(i - 1) == Some(&b'\r') {
41                i - 1
42            } else {
43                i
44            };
45            out.push((start, end));
46            start = i + 1;
47        }
48    }
49    if start < bytes.len() {
50        let end = if bytes.last() == Some(&b'\r') {
51            bytes.len() - 1
52        } else {
53            bytes.len()
54        };
55        out.push((start, end));
56    }
57    out
58}
59
60/// Split a whole-file byte `range` across the lines it covers, calling `emit`
61/// with each line's index and the range clamped and rebased to that line's
62/// visible region. `starts` is the first column of `line_bounds`.
63pub(crate) fn split_range_by_line(
64    bounds: &[(usize, usize)],
65    starts: &[usize],
66    range: &Range<usize>,
67    mut emit: impl FnMut(usize, Range<usize>),
68) {
69    let mut li = match starts.binary_search(&range.start) {
70        Ok(i) => i,
71        Err(i) => i.saturating_sub(1),
72    };
73    while let Some(&(ls, le)) = bounds.get(li) {
74        if ls >= range.end {
75            break;
76        }
77        let s = range.start.max(ls);
78        let e = range.end.min(le);
79        if s < e {
80            emit(li, (s - ls)..(e - ls));
81        }
82        li += 1;
83    }
84}