Skip to main content

meta_language/
incremental.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2use std::sync::Arc;
3
4use crate::{
5    tree_sitter_adapter, ByteRange, Link, LinkId, LinkMetadata, LinkNetwork, LinkType,
6    ParseConfiguration,
7};
8
9impl LinkNetwork {
10    /// Applies a byte-range source edit and reparses the network.
11    ///
12    /// Tree-sitter-backed languages use tree-sitter's incremental parse path;
13    /// other languages fall back to the built-in lossless parser. Links whose
14    /// source spans are outside the replaced byte range keep their identifiers.
15    pub fn apply_edit(&mut self, range: ByteRange, replacement: &str) -> bool {
16        self.apply_edit_with_configuration(range, replacement, ParseConfiguration::default())
17    }
18
19    /// Applies a byte-range source edit using an explicit parse configuration.
20    ///
21    /// Returns `false` when the range is outside the reconstructed source text,
22    /// splits a UTF-8 code point, or the network has no document language.
23    pub fn apply_edit_with_configuration(
24        &mut self,
25        range: ByteRange,
26        replacement: &str,
27        configuration: ParseConfiguration,
28    ) -> bool {
29        let old_text = self.reconstruct_text();
30        let Some(edited_text) = apply_text_edit(&old_text, range, replacement) else {
31            return false;
32        };
33        let Some(language) = document_language(self).map(ToOwned::to_owned) else {
34            return false;
35        };
36
37        let reparsed = tree_sitter_adapter::parse_incremental(
38            &old_text,
39            range,
40            replacement,
41            &language,
42            configuration,
43        )
44        .unwrap_or_else(|| Self::parse(&edited_text, &language, configuration));
45
46        let edit = AppliedEdit::new(range, replacement.len());
47        *self = remap_reparsed_network(self, &reparsed, edit);
48        true
49    }
50}
51
52fn document_language(network: &LinkNetwork) -> Option<&str> {
53    network
54        .links()
55        .find(|link| link.metadata().link_type() == Some(LinkType::Document))
56        .and_then(|link| link.metadata().language())
57}
58
59fn remap_reparsed_network(
60    old: &LinkNetwork,
61    reparsed: &LinkNetwork,
62    edit: AppliedEdit,
63) -> LinkNetwork {
64    let mut id_map = stable_id_map(old, reparsed, edit);
65    let mut used_targets = id_map.values().copied().collect::<BTreeSet<_>>();
66    let mut next_id = old.next_id.max(reparsed.next_id);
67
68    for link in reparsed.links() {
69        if id_map.contains_key(&link.id()) {
70            continue;
71        }
72
73        let target = if used_targets.contains(&link.id()) {
74            let fresh = next_unused_id(&mut next_id, &used_targets);
75            used_targets.insert(fresh);
76            fresh
77        } else {
78            used_targets.insert(link.id());
79            link.id()
80        };
81        id_map.insert(link.id(), target);
82    }
83
84    let mut links = BTreeMap::new();
85    for link in reparsed.links() {
86        let id = id_map[&link.id()];
87        let references = link
88            .references()
89            .iter()
90            .map(|reference| id_map[reference])
91            .collect::<Vec<_>>();
92        let candidate = Link {
93            id,
94            references: Arc::from(references),
95            metadata: link.metadata().clone(),
96        };
97
98        if let Some(shared) = old.links.get(&id) {
99            if shared.as_ref() == &candidate {
100                links.insert(id, Arc::clone(shared));
101                continue;
102            }
103        }
104
105        links.insert(id, Arc::new(candidate));
106    }
107
108    let terms = reparsed
109        .terms
110        .iter()
111        .filter_map(|(term, id)| id_map.get(id).map(|mapped| (Arc::clone(term), *mapped)))
112        .collect();
113    let next_id = used_targets
114        .iter()
115        .map(|id| id.as_u64())
116        .max()
117        .map_or(1, |id| id + 1);
118
119    LinkNetwork {
120        next_id,
121        links,
122        terms,
123        concept_syntax: reparsed.concept_syntax.clone(),
124        strings: reparsed.strings.clone(),
125    }
126}
127
128fn stable_id_map(
129    old: &LinkNetwork,
130    reparsed: &LinkNetwork,
131    edit: AppliedEdit,
132) -> BTreeMap<LinkId, LinkId> {
133    let mut old_links = OldLinkIndex::new(old, edit);
134    let mut mapped = BTreeMap::new();
135
136    for new_link in reparsed.links() {
137        let key = MatchKey::of(new_link.metadata());
138        let matched = match new_link.metadata().span() {
139            Some(span) => old_links.take_spanned(key, span.byte_range()),
140            None => old_links.take_unspanned(key, new_link),
141        };
142        if let Some(old_id) = matched {
143            mapped.insert(new_link.id(), old_id);
144        }
145    }
146
147    // A link without a span is recognized by the identifiers it references, so
148    // it can only be matched once everything it references has been. Repeat
149    // until a round maps nothing new.
150    let mut pending = reparsed
151        .links()
152        .filter(|link| link.metadata().span().is_none() && !mapped.contains_key(&link.id()))
153        .collect::<Vec<_>>();
154    while !pending.is_empty() {
155        let remaining = pending.len();
156        let mut unmatched = Vec::with_capacity(remaining);
157        for new_link in pending {
158            let matched = new_link
159                .references()
160                .iter()
161                .map(|reference| mapped.get(reference).copied())
162                .collect::<Option<Vec<_>>>()
163                .and_then(|references| {
164                    old_links.take_referencing(MatchKey::of(new_link.metadata()), references)
165                });
166            match matched {
167                Some(old_id) => {
168                    mapped.insert(new_link.id(), old_id);
169                }
170                None => unmatched.push(new_link),
171            }
172        }
173        if unmatched.len() == remaining {
174            break;
175        }
176        pending = unmatched;
177    }
178
179    mapped
180}
181
182/// Metadata two links must share to keep one identifier, shaped as a map key so
183/// that candidates can be looked up instead of searched for.
184#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
185struct MatchKey<'a> {
186    link_type: Option<LinkType>,
187    named: bool,
188    term: Option<&'a str>,
189    definition: Option<&'a str>,
190    language: Option<&'a str>,
191    /// Parse flags spelled out as booleans, because `LinkFlags` carries no
192    /// ordering of its own.
193    flags: (bool, bool, bool, bool),
194}
195
196impl<'a> MatchKey<'a> {
197    fn of(metadata: &'a LinkMetadata) -> Self {
198        let flags = metadata.flags();
199        Self {
200            link_type: metadata.link_type(),
201            named: metadata.is_named(),
202            term: metadata.term(),
203            definition: metadata.definition(),
204            language: metadata.language(),
205            flags: (
206                flags.is_error(),
207                flags.has_error(),
208                flags.is_missing(),
209                flags.is_extra(),
210            ),
211        }
212    }
213}
214
215/// Identifiers of the previous network's links, grouped by everything a match
216/// requires.
217///
218/// Reparsing produces about as many links as the previous parse held, so
219/// searching every old link for every new one costs `O(old * new)` and made a
220/// 15 KB edit take twenty seconds (issue #193). Every group keeps its links in
221/// ascending identifier order and hands them out from the front, which is the
222/// link a search over `old.links()` would have stopped at first.
223struct OldLinkIndex<'a> {
224    /// Links carrying a span, keyed by the span the edit moves them to.
225    spanned: BTreeMap<(MatchKey<'a>, usize, usize), VecDeque<LinkId>>,
226    /// Links carrying no span, keyed by the identifiers they reference.
227    referencing: BTreeMap<(MatchKey<'a>, Vec<LinkId>), VecDeque<LinkId>>,
228    /// Links carrying no span that reference themselves and nothing else.
229    self_referencing: BTreeMap<MatchKey<'a>, VecDeque<LinkId>>,
230    /// Links already handed out, which every group skips.
231    used: BTreeSet<LinkId>,
232}
233
234impl<'a> OldLinkIndex<'a> {
235    fn new(old: &'a LinkNetwork, edit: AppliedEdit) -> Self {
236        let mut index = Self {
237            spanned: BTreeMap::new(),
238            referencing: BTreeMap::new(),
239            self_referencing: BTreeMap::new(),
240            used: BTreeSet::new(),
241        };
242
243        for link in old.links() {
244            let key = MatchKey::of(link.metadata());
245            if let Some(span) = link.metadata().span() {
246                // Links the edit overlaps cannot keep their identifier.
247                if let Some(range) = edit.adjusted_range(span.byte_range()) {
248                    index
249                        .spanned
250                        .entry((key, range.start(), range.end()))
251                        .or_default()
252                        .push_back(link.id());
253                }
254                continue;
255            }
256
257            index
258                .referencing
259                .entry((key, link.references().to_vec()))
260                .or_default()
261                .push_back(link.id());
262            if is_self_reference(link) {
263                index
264                    .self_referencing
265                    .entry(key)
266                    .or_default()
267                    .push_back(link.id());
268            }
269        }
270
271        index
272    }
273
274    /// Identifier of an unused old link whose shifted span is `range`.
275    fn take_spanned(&mut self, key: MatchKey<'a>, range: ByteRange) -> Option<LinkId> {
276        let matched = Self::front(
277            self.spanned.get_mut(&(key, range.start(), range.end())),
278            &self.used,
279        )?;
280        self.used.insert(matched);
281        Some(matched)
282    }
283
284    /// Identifier of an unused old link that carries no span and either
285    /// references exactly what `new_link` references or, when `new_link`
286    /// references only itself, references only itself as well.
287    fn take_unspanned(&mut self, key: MatchKey<'a>, new_link: &Link) -> Option<LinkId> {
288        let by_references = Self::front(
289            self.referencing
290                .get_mut(&(key, new_link.references().to_vec())),
291            &self.used,
292        );
293        let by_self_reference = if is_self_reference(new_link) {
294            Self::front(self.self_referencing.get_mut(&key), &self.used)
295        } else {
296            None
297        };
298
299        // A search over `old.links()` would have stopped at whichever of the
300        // two candidates was inserted first, which is the smaller identifier.
301        let matched = by_references.into_iter().chain(by_self_reference).min()?;
302        self.used.insert(matched);
303        Some(matched)
304    }
305
306    /// Identifier of an unused old link that carries no span and references
307    /// exactly `references`.
308    fn take_referencing(&mut self, key: MatchKey<'a>, references: Vec<LinkId>) -> Option<LinkId> {
309        let matched = Self::front(self.referencing.get_mut(&(key, references)), &self.used)?;
310        self.used.insert(matched);
311        Some(matched)
312    }
313
314    /// First unused identifier in `group`, dropping the used ones it skips.
315    fn front(group: Option<&mut VecDeque<LinkId>>, used: &BTreeSet<LinkId>) -> Option<LinkId> {
316        let group = group?;
317        while group.front().is_some_and(|id| used.contains(id)) {
318            group.pop_front();
319        }
320        group.front().copied()
321    }
322}
323
324fn apply_text_edit(old_text: &str, range: ByteRange, replacement: &str) -> Option<String> {
325    if range.end() > old_text.len()
326        || !old_text.is_char_boundary(range.start())
327        || !old_text.is_char_boundary(range.end())
328    {
329        return None;
330    }
331
332    let mut edited =
333        String::with_capacity(old_text.len() - (range.end() - range.start()) + replacement.len());
334    edited.push_str(&old_text[..range.start()]);
335    edited.push_str(replacement);
336    edited.push_str(&old_text[range.end()..]);
337    Some(edited)
338}
339
340fn next_unused_id(next_id: &mut u64, used_targets: &BTreeSet<LinkId>) -> LinkId {
341    loop {
342        let candidate = LinkId(*next_id);
343        *next_id += 1;
344        if !used_targets.contains(&candidate) {
345            return candidate;
346        }
347    }
348}
349
350fn is_self_reference(link: &Link) -> bool {
351    link.references() == [link.id()]
352}
353
354#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355struct AppliedEdit {
356    old_start: usize,
357    old_end: usize,
358    new_end: usize,
359}
360
361impl AppliedEdit {
362    const fn new(range: ByteRange, replacement_len: usize) -> Self {
363        Self {
364            old_start: range.start(),
365            old_end: range.end(),
366            new_end: range.start() + replacement_len,
367        }
368    }
369
370    const fn adjusted_range(self, range: ByteRange) -> Option<ByteRange> {
371        if range.end() <= self.old_start {
372            return Some(range);
373        }
374        if range.start() < self.old_end {
375            return None;
376        }
377
378        let start = shift_byte(range.start(), self.old_end, self.new_end);
379        let end = shift_byte(range.end(), self.old_end, self.new_end);
380        Some(ByteRange::new(start, end))
381    }
382}
383
384const fn shift_byte(byte: usize, old_end: usize, new_end: usize) -> usize {
385    if new_end >= old_end {
386        byte + (new_end - old_end)
387    } else {
388        byte - (old_end - new_end)
389    }
390}