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