Skip to main content

makefile_lossless/
incremental.rs

1//! Incremental reparsing support for efficient handling of text edits.
2//!
3//! Instead of reparsing the entire file after each edit, this module identifies
4//! which top-level items are affected and reparses only those, splicing the
5//! results back into the existing green tree.
6
7use crate::lossless::{ErrorInfo, Makefile, PositionedParseError};
8use crate::parse::Parse;
9use rowan::TextRange;
10
11/// A text edit applied to the source, as typically received from an LSP.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TextEdit {
14    /// The byte range in the old text to replace.
15    pub range: TextRange,
16    /// The new text to insert in place of the range.
17    pub new_text: String,
18}
19
20impl TextEdit {
21    /// Create a new text edit.
22    pub fn new(range: TextRange, new_text: String) -> Self {
23        Self { range, new_text }
24    }
25
26    /// The length delta introduced by this edit.
27    fn delta(&self) -> i64 {
28        self.new_text.len() as i64 - u32::from(self.range.len()) as i64
29    }
30}
31
32/// Apply a text edit to the old source, producing the new source text.
33pub fn apply_edit_to_text(old_text: &str, edit: &TextEdit) -> String {
34    let start: usize = u32::from(edit.range.start()) as usize;
35    let end: usize = u32::from(edit.range.end()) as usize;
36    let mut new = String::with_capacity(old_text.len().wrapping_add_signed(edit.delta() as isize));
37    new.push_str(&old_text[..start]);
38    new.push_str(&edit.new_text);
39    new.push_str(&old_text[end..]);
40    new
41}
42
43impl Parse<Makefile> {
44    /// Apply an incremental text edit and reparse only the affected region.
45    ///
46    /// This is more efficient than a full reparse for large files, as it reuses
47    /// the green tree nodes for unaffected top-level items.
48    ///
49    /// # Arguments
50    /// * `old_text` - The full text before the edit
51    /// * `edit` - The edit to apply
52    ///
53    /// # Returns
54    /// A new `Parse<Makefile>` with the edit applied, and the new full text.
55    ///
56    /// # Example
57    /// ```
58    /// use makefile_lossless::{Makefile, Parse, TextEdit, TextRange};
59    ///
60    /// let old_text = "VAR1 = old\nVAR2 = value\n";
61    /// let parse = Parse::<Makefile>::parse_makefile(old_text);
62    ///
63    /// // Change "old" to "new" in VAR1
64    /// let edit = TextEdit::new(
65    ///     TextRange::new(7.into(), 10.into()),
66    ///     "new".to_string(),
67    /// );
68    /// let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
69    /// assert_eq!(new_text, "VAR1 = new\nVAR2 = value\n");
70    ///
71    /// let makefile: Makefile = new_parse.tree();
72    /// let vars: Vec<_> = makefile.variable_definitions().collect();
73    /// assert_eq!(vars.len(), 2);
74    /// assert_eq!(vars[0].raw_value(), Some("new".to_string()));
75    /// assert_eq!(vars[1].raw_value(), Some("value".to_string()));
76    /// ```
77    pub fn apply_edit(&self, old_text: &str, edit: &TextEdit) -> (Self, String) {
78        let new_text = apply_edit_to_text(old_text, edit);
79        let delta = edit.delta();
80
81        // Walk the ROOT green node's children to find which ones overlap the edit range.
82        let old_green = self.green();
83        let children: Vec<_> = old_green.children().map(|c| c.to_owned()).collect();
84
85        // Compute the text range of each direct child of ROOT.
86        let mut child_ranges: Vec<TextRange> = Vec::with_capacity(children.len());
87        let mut offset = rowan::TextSize::from(0);
88        for child in &children {
89            let len = match child {
90                rowan::NodeOrToken::Node(n) => n.text_len(),
91                rowan::NodeOrToken::Token(t) => t.text_len(),
92            };
93            child_ranges.push(TextRange::new(offset, offset + len));
94            offset += len;
95        }
96
97        if children.is_empty() {
98            // Empty tree — just do a full parse.
99            let new_parse = Parse::parse_makefile(&new_text);
100            return (new_parse, new_text);
101        }
102
103        // Find the first and last children that overlap or are adjacent to the edit range.
104        let first_affected = child_ranges
105            .iter()
106            .position(|r| r.end() > edit.range.start())
107            .unwrap_or(children.len().saturating_sub(1));
108
109        let last_affected = child_ranges
110            .iter()
111            .rposition(|r| r.start() < edit.range.end())
112            .unwrap_or(first_affected);
113
114        // The reparse region in the *old* text.
115        let reparse_start = child_ranges[first_affected].start();
116        let reparse_end_old = child_ranges[last_affected].end();
117
118        // The corresponding region in the *new* text.
119        // Everything before first_affected is unchanged, so reparse_start is the same.
120        // The end shifts by the delta.
121        let reparse_end_new =
122            rowan::TextSize::from((u32::from(reparse_end_old) as i64 + delta) as u32);
123
124        let reparse_region =
125            &new_text[u32::from(reparse_start) as usize..u32::from(reparse_end_new) as usize];
126
127        // Reparse just the affected region.
128        let reparsed = crate::lossless::parse(reparse_region, None);
129        let reparsed_root = reparsed.green_node;
130
131        // Build the new ROOT by splicing: [old children before] + [reparsed children] + [old children after]
132        let new_root = old_green.splice_children(
133            first_affected..last_affected + 1,
134            reparsed_root.children().map(|c| c.to_owned()),
135        );
136
137        // Rebuild errors: keep errors outside the affected region, add reparsed errors with adjusted positions.
138        let mut new_errors = Vec::new();
139        let mut new_positioned_errors = Vec::new();
140
141        // Errors before the affected region (unchanged).
142        for err in self.errors() {
143            // ErrorInfo uses line numbers, not byte offsets. We need to figure out
144            // which errors are before the reparse region by line number.
145            // Since we can't easily map line numbers to byte offsets without the text,
146            // we use a simpler approach: count newlines up to reparse_start.
147            let lines_before = old_text[..u32::from(reparse_start) as usize]
148                .matches('\n')
149                .count();
150            if err.line <= lines_before {
151                new_errors.push(err.clone());
152            }
153        }
154
155        // Errors from the reparsed region (adjusted line numbers).
156        let line_offset = old_text[..u32::from(reparse_start) as usize]
157            .matches('\n')
158            .count();
159        for err in &reparsed.errors {
160            new_errors.push(ErrorInfo {
161                message: err.message.clone(),
162                line: err.line + line_offset,
163                context: err.context.clone(),
164            });
165        }
166
167        // Errors after the affected region (adjusted line numbers).
168        let old_lines_in_region = old_text
169            [u32::from(reparse_start) as usize..u32::from(reparse_end_old) as usize]
170            .matches('\n')
171            .count();
172        let new_lines_in_region = reparse_region.matches('\n').count();
173        let line_delta = new_lines_in_region as i64 - old_lines_in_region as i64;
174        let lines_after_start = line_offset + old_lines_in_region;
175        for err in self.errors() {
176            if err.line > lines_after_start {
177                new_errors.push(ErrorInfo {
178                    message: err.message.clone(),
179                    line: (err.line as i64 + line_delta) as usize,
180                    context: err.context.clone(),
181                });
182            }
183        }
184
185        // Positioned errors before.
186        for err in self.positioned_errors() {
187            if err.range.end() <= reparse_start {
188                new_positioned_errors.push(err.clone());
189            }
190        }
191
192        // Positioned errors from reparsed region (shifted by reparse_start).
193        for err in &reparsed.positioned_errors {
194            new_positioned_errors.push(PositionedParseError {
195                message: err.message.clone(),
196                range: TextRange::new(
197                    err.range.start() + reparse_start,
198                    err.range.end() + reparse_start,
199                ),
200                code: err.code.clone(),
201            });
202        }
203
204        // Positioned errors after (shifted by delta).
205        for err in self.positioned_errors() {
206            if err.range.start() >= reparse_end_old {
207                let shift = rowan::TextSize::from(delta.unsigned_abs() as u32);
208                let (new_start, new_end) = if delta >= 0 {
209                    (err.range.start() + shift, err.range.end() + shift)
210                } else {
211                    (err.range.start() - shift, err.range.end() - shift)
212                };
213                new_positioned_errors.push(PositionedParseError {
214                    message: err.message.clone(),
215                    range: TextRange::new(new_start, new_end),
216                    code: err.code.clone(),
217                });
218            }
219        }
220
221        let new_parse = Parse::new(new_root, new_errors, new_positioned_errors);
222        (new_parse, new_text)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use rowan::ast::AstNode;
230
231    #[test]
232    fn test_apply_edit_to_text() {
233        let old = "hello world";
234        let edit = TextEdit::new(TextRange::new(6.into(), 11.into()), "rust".to_string());
235        assert_eq!(apply_edit_to_text(old, &edit), "hello rust");
236    }
237
238    #[test]
239    fn test_apply_edit_to_text_insert() {
240        let old = "hello world";
241        let edit = TextEdit::new(TextRange::new(5.into(), 5.into()), " beautiful".to_string());
242        assert_eq!(apply_edit_to_text(old, &edit), "hello beautiful world");
243    }
244
245    #[test]
246    fn test_apply_edit_to_text_delete() {
247        let old = "hello world";
248        let edit = TextEdit::new(TextRange::new(5.into(), 11.into()), String::new());
249        assert_eq!(apply_edit_to_text(old, &edit), "hello");
250    }
251
252    #[test]
253    fn test_incremental_change_variable_value() {
254        let old_text = "VAR1 = old\nVAR2 = value\n";
255        let parse = Parse::parse_makefile(old_text);
256
257        // Change "old" to "new"
258        let edit = TextEdit::new(TextRange::new(7.into(), 10.into()), "new".to_string());
259        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
260
261        assert_eq!(new_text, "VAR1 = new\nVAR2 = value\n");
262        let makefile = new_parse.tree();
263        let vars: Vec<_> = makefile.variable_definitions().collect();
264        assert_eq!(vars.len(), 2);
265        assert_eq!(vars[0].name(), Some("VAR1".to_string()));
266        assert_eq!(vars[0].raw_value(), Some("new".to_string()));
267        assert_eq!(vars[1].name(), Some("VAR2".to_string()));
268        assert_eq!(vars[1].raw_value(), Some("value".to_string()));
269    }
270
271    #[test]
272    fn test_incremental_change_rule_command() {
273        let old_text = "all:\n\techo hello\n";
274        let parse = Parse::parse_makefile(old_text);
275
276        // Change "hello" to "goodbye"
277        let edit = TextEdit::new(TextRange::new(11.into(), 16.into()), "goodbye".to_string());
278        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
279
280        assert_eq!(new_text, "all:\n\techo goodbye\n");
281        let makefile = new_parse.tree();
282        assert_eq!(makefile.rules().count(), 1);
283    }
284
285    #[test]
286    fn test_incremental_insert_new_variable() {
287        let old_text = "VAR1 = one\nVAR2 = two\n";
288        let parse = Parse::parse_makefile(old_text);
289
290        // Insert a new variable between the two
291        let edit = TextEdit::new(
292            TextRange::new(11.into(), 11.into()),
293            "NEW = inserted\n".to_string(),
294        );
295        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
296
297        assert_eq!(new_text, "VAR1 = one\nNEW = inserted\nVAR2 = two\n");
298        let makefile = new_parse.tree();
299        let vars: Vec<_> = makefile.variable_definitions().collect();
300        assert_eq!(vars.len(), 3);
301        assert_eq!(vars[0].name(), Some("VAR1".to_string()));
302        assert_eq!(vars[1].name(), Some("NEW".to_string()));
303        assert_eq!(vars[2].name(), Some("VAR2".to_string()));
304    }
305
306    #[test]
307    fn test_incremental_delete_variable() {
308        let old_text = "VAR1 = one\nVAR2 = two\nVAR3 = three\n";
309        let parse = Parse::parse_makefile(old_text);
310
311        // Delete VAR2 line
312        let edit = TextEdit::new(TextRange::new(11.into(), 22.into()), String::new());
313        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
314
315        assert_eq!(new_text, "VAR1 = one\nVAR3 = three\n");
316        let makefile = new_parse.tree();
317        let vars: Vec<_> = makefile.variable_definitions().collect();
318        assert_eq!(vars.len(), 2);
319        assert_eq!(vars[0].name(), Some("VAR1".to_string()));
320        assert_eq!(vars[1].name(), Some("VAR3".to_string()));
321    }
322
323    #[test]
324    fn test_incremental_edit_preserves_unaffected() {
325        let old_text = "VAR1 = one\nVAR2 = two\nVAR3 = three\n";
326        let parse = Parse::parse_makefile(old_text);
327
328        // Only change VAR2's value
329        let edit = TextEdit::new(TextRange::new(18.into(), 21.into()), "TWO".to_string());
330        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
331
332        assert_eq!(new_text, "VAR1 = one\nVAR2 = TWO\nVAR3 = three\n");
333        let makefile = new_parse.tree();
334        let vars: Vec<_> = makefile.variable_definitions().collect();
335        assert_eq!(vars.len(), 3);
336
337        // Verify that unaffected green nodes are structurally identical.
338        let old_children: Vec<_> = parse.green().children().map(|c| c.to_owned()).collect();
339        let new_children: Vec<_> = new_parse.green().children().map(|c| c.to_owned()).collect();
340
341        // First child (VAR1 node) should be structurally identical.
342        assert_eq!(
343            old_children[0], new_children[0],
344            "VAR1 green node should be identical (reused)"
345        );
346    }
347
348    #[test]
349    fn test_incremental_empty_file() {
350        let old_text = "";
351        let parse = Parse::parse_makefile(old_text);
352
353        let edit = TextEdit::new(
354            TextRange::new(0.into(), 0.into()),
355            "VAR = value\n".to_string(),
356        );
357        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
358
359        assert_eq!(new_text, "VAR = value\n");
360        let makefile = new_parse.tree();
361        assert_eq!(makefile.variable_definitions().count(), 1);
362    }
363
364    #[test]
365    fn test_incremental_change_variable_to_rule() {
366        let old_text = "target = value\n";
367        let parse = Parse::parse_makefile(old_text);
368
369        // Change "= value" to ":"
370        let edit = TextEdit::new(TextRange::new(7.into(), 14.into()), ":".to_string());
371        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
372
373        assert_eq!(new_text, "target :\n");
374        let makefile = new_parse.tree();
375        assert_eq!(makefile.variable_definitions().count(), 0);
376        assert_eq!(makefile.rules().count(), 1);
377    }
378
379    #[test]
380    fn test_incremental_matches_full_reparse() {
381        let old_text = "VAR1 = one\nall: dep\n\techo $(VAR1)\nVAR2 = two\n";
382        let parse = Parse::parse_makefile(old_text);
383
384        // Edit inside the rule
385        let edit = TextEdit::new(TextRange::new(26.into(), 30.into()), "VAR2".to_string());
386        let (incremental, new_text) = parse.apply_edit(old_text, &edit);
387        let full = Parse::parse_makefile(&new_text);
388
389        // Both should produce the same tree.
390        let inc_tree = incremental.tree();
391        let full_tree: Makefile = full.tree();
392        assert_eq!(
393            inc_tree.syntax().to_string(),
394            full_tree.syntax().to_string()
395        );
396    }
397
398    #[test]
399    fn test_incremental_edit_at_end() {
400        let old_text = "VAR = value\n";
401        let parse = Parse::parse_makefile(old_text);
402
403        // Append a new rule at the end
404        let edit = TextEdit::new(
405            TextRange::new(12.into(), 12.into()),
406            "all:\n\techo done\n".to_string(),
407        );
408        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
409
410        assert_eq!(new_text, "VAR = value\nall:\n\techo done\n");
411        let makefile = new_parse.tree();
412        assert_eq!(makefile.variable_definitions().count(), 1);
413        assert_eq!(makefile.rules().count(), 1);
414    }
415
416    #[test]
417    fn test_incremental_positioned_errors_shifted() {
418        // Verify that positioned errors from the reparsed region get correct offsets
419        let old_text = "VAR1 = one\nVAR2 = two\n";
420        let parse = Parse::parse_makefile(old_text);
421        assert!(parse.ok());
422
423        // Insert text that causes a parse error (indented line not in a rule)
424        let edit = TextEdit::new(
425            TextRange::new(11.into(), 11.into()),
426            "\tbad line\n".to_string(),
427        );
428        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
429        assert_eq!(new_text, "VAR1 = one\n\tbad line\nVAR2 = two\n");
430
431        // Full reparse should produce the same error count
432        let full = Parse::parse_makefile(&new_text);
433        assert_eq!(new_parse.errors().len(), full.errors().len());
434    }
435
436    #[test]
437    fn test_incremental_with_include() {
438        let old_text = "include foo.mk\nVAR = value\n";
439        let parse = Parse::parse_makefile(old_text);
440
441        // Change include path
442        let edit = TextEdit::new(TextRange::new(8.into(), 14.into()), "bar.mk".to_string());
443        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
444
445        assert_eq!(new_text, "include bar.mk\nVAR = value\n");
446        let makefile = new_parse.tree();
447        let includes: Vec<_> = makefile.includes().collect();
448        assert_eq!(includes.len(), 1);
449        assert_eq!(includes[0].path(), Some("bar.mk".to_string()));
450        assert_eq!(makefile.variable_definitions().count(), 1);
451    }
452
453    #[test]
454    fn test_incremental_with_conditional() {
455        let old_text = "ifdef DEBUG\nCFLAGS = -g\nendif\nVAR = value\n";
456        let parse = Parse::parse_makefile(old_text);
457
458        // Change variable inside conditional
459        let edit = TextEdit::new(TextRange::new(21.into(), 23.into()), "-O2".to_string());
460        let (new_parse, new_text) = parse.apply_edit(old_text, &edit);
461
462        assert_eq!(new_text, "ifdef DEBUG\nCFLAGS = -O2\nendif\nVAR = value\n");
463        let makefile = new_parse.tree();
464        assert_eq!(makefile.conditionals().count(), 1);
465        // 2 variables: CFLAGS inside conditional + VAR at top level
466        assert_eq!(makefile.variable_definitions().count(), 2);
467    }
468
469    #[test]
470    fn test_incremental_multiple_edits_sequentially() {
471        let old_text = "VAR1 = one\nVAR2 = two\nVAR3 = three\n";
472        let parse = Parse::parse_makefile(old_text);
473
474        // First edit: change VAR1
475        let edit1 = TextEdit::new(TextRange::new(7.into(), 10.into()), "ONE".to_string());
476        let (parse2, text2) = parse.apply_edit(old_text, &edit1);
477
478        // Second edit: change VAR3 (in the new text)
479        let edit2 = TextEdit::new(TextRange::new(29.into(), 34.into()), "THREE".to_string());
480        let (parse3, text3) = parse2.apply_edit(&text2, &edit2);
481
482        assert_eq!(text3, "VAR1 = ONE\nVAR2 = two\nVAR3 = THREE\n");
483        let makefile = parse3.tree();
484        let vars: Vec<_> = makefile.variable_definitions().collect();
485        assert_eq!(vars[0].raw_value(), Some("ONE".to_string()));
486        assert_eq!(vars[1].raw_value(), Some("two".to_string()));
487        assert_eq!(vars[2].raw_value(), Some("THREE".to_string()));
488    }
489}