Skip to main content

neco_editor/
lib.rs

1pub use neco_decor;
2pub use neco_diffcore;
3pub use neco_editor_search;
4pub use neco_editor_viewport;
5pub use neco_filetree;
6pub use neco_history;
7pub use neco_pathrel;
8pub use neco_textpatch;
9pub use neco_textview;
10pub use neco_tree;
11pub use neco_watchnorm;
12pub use neco_wrap;
13
14pub use neco_textview::RangeChange;
15
16use neco_decor::DecorationSet;
17use neco_history::EditHistory;
18use neco_textpatch::{TextPatch, TextPatchError};
19use neco_textview::LineIndex;
20use neco_wrap::{WrapMap, WrapPolicy};
21
22/// Detected indentation style of a text buffer.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum IndentStyle {
25    Tabs,
26    Spaces(u32),
27}
28
29pub struct EditorBuffer {
30    text: String,
31    line_index: LineIndex,
32}
33
34impl EditorBuffer {
35    pub fn new(text: String) -> Self {
36        Self {
37            line_index: LineIndex::new(&text),
38            text,
39        }
40    }
41
42    pub fn text(&self) -> &str {
43        &self.text
44    }
45
46    pub fn line_index(&self) -> &LineIndex {
47        &self.line_index
48    }
49
50    /// Detects the predominant indentation style by analyzing the first `sample_lines` lines.
51    /// Returns `Spaces(4)` as default when detection is inconclusive.
52    pub fn detect_indent(&self, sample_lines: usize) -> IndentStyle {
53        let mut tab_count: usize = 0;
54        let mut space_count: usize = 0;
55        let mut space_widths: Vec<u32> = Vec::new();
56
57        for line in self.text.lines().take(sample_lines) {
58            if line.is_empty() {
59                continue;
60            }
61            let first_non_ws = match line.find(|c: char| c != ' ' && c != '\t') {
62                Some(pos) if pos > 0 => pos,
63                _ => continue,
64            };
65            let first_char = line.as_bytes()[0];
66            if first_char == b'\t' {
67                tab_count += 1;
68            } else if first_char == b' ' {
69                space_count += 1;
70                let width =
71                    u32::try_from(first_non_ws).expect("leading space count should fit in u32");
72                space_widths.push(width);
73            }
74        }
75
76        if tab_count == 0 && space_count == 0 {
77            return IndentStyle::Spaces(4);
78        }
79
80        if tab_count >= space_count {
81            IndentStyle::Tabs
82        } else {
83            // Find GCD of all non-zero leading space counts to determine indent width.
84            let gcd = space_widths.iter().copied().fold(0u32, gcd_u32);
85            if gcd == 0 {
86                IndentStyle::Spaces(4)
87            } else {
88                IndentStyle::Spaces(gcd)
89            }
90        }
91    }
92
93    /// Apply patches, update text and LineIndex, return RangeChanges for downstream consumers.
94    pub fn apply_patches(
95        &mut self,
96        patches: &[TextPatch],
97    ) -> Result<Vec<RangeChange>, TextPatchError> {
98        let new_text = neco_textpatch::apply_patches(self.text(), patches)?;
99        let range_changes = build_range_changes(patches);
100        self.text = new_text;
101        self.line_index = LineIndex::new(&self.text);
102        Ok(range_changes)
103    }
104
105    /// Apply patches and propagate to all subsystems in order:
106    /// 1. Record to history (before text change, needs original text for inverse patch computation)
107    /// 2. Apply patches to text and rebuild LineIndex
108    /// 3. Map decorations through changes
109    /// 4. Update wrap map (only when both wrap_map and wrap_policy are Some)
110    pub fn apply_patches_with(
111        &mut self,
112        patches: &[TextPatch],
113        decorations: &mut DecorationSet,
114        wrap_map: Option<&mut WrapMap>,
115        wrap_policy: Option<&WrapPolicy>,
116        history: Option<&mut EditHistory>,
117        label: Option<&str>,
118    ) -> Result<(), TextPatchError> {
119        let old_line_index = if wrap_map.is_some() {
120            Some(self.line_index.clone())
121        } else {
122            None
123        };
124
125        if let Some(history) = history {
126            history.push_edit(label.unwrap_or(""), self.text(), patches.to_vec());
127        }
128
129        let range_changes = self.apply_patches(patches)?;
130        decorations.map_through_changes(&range_changes);
131
132        if let (Some(wrap_map), Some(wrap_policy)) = (wrap_map, wrap_policy) {
133            update_wrap_map(
134                wrap_map,
135                wrap_policy,
136                old_line_index
137                    .as_ref()
138                    .expect("old_line_index set when wrap_map is Some"),
139                &self.text,
140                &self.line_index,
141                patches,
142                &range_changes,
143            );
144        }
145
146        Ok(())
147    }
148}
149
150fn build_range_changes(patches: &[TextPatch]) -> Vec<RangeChange> {
151    let mut ordered = patches.iter().enumerate().collect::<Vec<_>>();
152    ordered.sort_by(|(left_index, left_patch), (right_index, right_patch)| {
153        left_patch
154            .start()
155            .cmp(&right_patch.start())
156            .then_with(|| left_patch.end().cmp(&right_patch.end()))
157            .then_with(|| left_index.cmp(right_index))
158    });
159
160    let mut cumulative_delta = 0i64;
161    let mut changes = Vec::with_capacity(ordered.len());
162
163    for (_, patch) in ordered {
164        let patch_start = i64::try_from(patch.start()).expect("patch start exceeds i64::MAX");
165        let patch_end = i64::try_from(patch.end()).expect("patch end exceeds i64::MAX");
166        let replacement_len =
167            i64::try_from(patch.replacement().len()).expect("replacement len exceeds i64::MAX");
168
169        let adjusted_start = usize::try_from(patch_start + cumulative_delta)
170            .expect("validated patch start should stay non-negative");
171        let adjusted_old_end = usize::try_from(patch_end + cumulative_delta)
172            .expect("validated patch end should stay non-negative");
173        let adjusted_new_end = adjusted_start
174            .checked_add(usize::try_from(replacement_len).expect("replacement len exceeds usize"))
175            .expect("range change new end overflow");
176
177        changes.push(RangeChange::new(
178            adjusted_start,
179            adjusted_old_end,
180            adjusted_new_end,
181        ));
182
183        cumulative_delta += replacement_len - (patch_end - patch_start);
184    }
185
186    changes
187}
188
189fn update_wrap_map(
190    wrap_map: &mut WrapMap,
191    wrap_policy: &WrapPolicy,
192    old_line_index: &LineIndex,
193    new_text: &str,
194    new_line_index: &LineIndex,
195    patches: &[TextPatch],
196    range_changes: &[RangeChange],
197) {
198    if patches.is_empty() {
199        return;
200    }
201
202    let start_offset = patches.iter().map(TextPatch::start).min().unwrap_or(0);
203    let old_end_offset = patches
204        .iter()
205        .map(TextPatch::end)
206        .max()
207        .unwrap_or(start_offset);
208    let new_end_offset = range_changes
209        .iter()
210        .map(RangeChange::new_end)
211        .max()
212        .unwrap_or(start_offset);
213
214    let start_line = old_line_index
215        .line_of_offset(start_offset)
216        .expect("validated patch start should map to a line");
217    let old_end_line = old_line_index
218        .line_of_offset(old_end_offset)
219        .expect("validated patch end should map to a line");
220    let new_end_line = new_line_index
221        .line_of_offset(new_end_offset)
222        .expect("validated patch end should map to a line");
223
224    let old_line_count = old_end_line - start_line + 1;
225    let new_line_count = new_end_line - start_line + 1;
226
227    if old_line_count == new_line_count {
228        for line in start_line..=new_end_line {
229            let line_text = line_text(new_text, new_line_index, line);
230            wrap_map.rewrap_line(line, line_text, wrap_policy);
231        }
232        return;
233    }
234
235    // start_line is computed from old_line_index. Because patches are sorted
236    // by start offset and applied front-to-back, no prior patch can shift the
237    // line number of the earliest affected offset. start_line is therefore
238    // valid in both old and new coordinate spaces.
239    let new_lines = collect_line_texts(new_text, new_line_index, start_line, new_line_count);
240
241    wrap_map.splice_lines(
242        start_line,
243        old_line_count,
244        new_lines.into_iter(),
245        wrap_policy,
246    );
247}
248
249fn collect_line_texts<'a>(
250    text: &'a str,
251    line_index: &LineIndex,
252    start_line: u32,
253    line_count: u32,
254) -> Vec<&'a str> {
255    (start_line..start_line + line_count)
256        .map(|line| line_text(text, line_index, line))
257        .collect()
258}
259
260fn line_text<'a>(text: &'a str, line_index: &LineIndex, line: u32) -> &'a str {
261    let range = line_index
262        .line_range(line)
263        .expect("line should be in range for wrap update");
264    &text[range.start()..range.end()]
265}
266
267// ---------------------------------------------------------------------------
268// Bracket matching
269// ---------------------------------------------------------------------------
270
271/// A matched pair of brackets at byte offsets.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub struct BracketPair {
274    open: usize,
275    close: usize,
276}
277
278impl BracketPair {
279    pub fn open(&self) -> usize {
280        self.open
281    }
282
283    pub fn close(&self) -> usize {
284        self.close
285    }
286}
287
288/// Returns the matching closing bracket for an opening bracket, or vice versa.
289fn matching_bracket(ch: char) -> Option<char> {
290    match ch {
291        '(' => Some(')'),
292        ')' => Some('('),
293        '[' => Some(']'),
294        ']' => Some('['),
295        '{' => Some('}'),
296        '}' => Some('{'),
297        _ => None,
298    }
299}
300
301/// Returns true if the character is an opening bracket.
302fn is_opening_bracket(ch: char) -> bool {
303    matches!(ch, '(' | '[' | '{')
304}
305
306/// Finds the matching bracket for the bracket character at `offset`.
307///
308/// Supports `()`, `[]`, `{}`. Returns `None` if `offset` is not on a
309/// bracket character, not on a valid char boundary, or no matching
310/// bracket is found.
311pub fn find_matching_bracket(text: &str, offset: usize) -> Option<BracketPair> {
312    if offset >= text.len() || !text.is_char_boundary(offset) {
313        return None;
314    }
315
316    let ch = text[offset..].chars().next()?;
317    let target = matching_bracket(ch)?;
318
319    if is_opening_bracket(ch) {
320        // Scan forward
321        let mut depth = 0usize;
322        let mut pos = offset;
323        for c in text[offset..].chars() {
324            if c == ch {
325                depth += 1;
326            } else if c == target {
327                depth -= 1;
328                if depth == 0 {
329                    return Some(BracketPair {
330                        open: offset,
331                        close: pos,
332                    });
333                }
334            }
335            pos += c.len_utf8();
336        }
337        None
338    } else {
339        // Closing bracket: scan backward
340        let mut depth = 0usize;
341        for (byte_pos, c) in text[..offset + ch.len_utf8()].char_indices().rev() {
342            if c == ch {
343                depth += 1;
344            } else if c == target {
345                depth -= 1;
346                if depth == 0 {
347                    return Some(BracketPair {
348                        open: byte_pos,
349                        close: offset,
350                    });
351                }
352            }
353        }
354        None
355    }
356}
357
358// ---------------------------------------------------------------------------
359// Auto-indent
360// ---------------------------------------------------------------------------
361
362/// Returns the leading whitespace of the line containing `offset`.
363///
364/// Used by the UI layer to replicate the current line's indentation on Enter.
365pub fn auto_indent(text: &str, line_index: &neco_textview::LineIndex, offset: usize) -> String {
366    let line = match line_index.line_of_offset(offset) {
367        Ok(l) => l,
368        Err(_) => return String::new(),
369    };
370    let range = match line_index.line_range(line) {
371        Ok(r) => r,
372        Err(_) => return String::new(),
373    };
374    let line_text = &text[range.start()..range.end()];
375    let indent_len = line_text
376        .chars()
377        .take_while(|c| *c == ' ' || *c == '\t')
378        .map(|c| c.len_utf8())
379        .sum::<usize>();
380    line_text[..indent_len].to_string()
381}
382
383// ---------------------------------------------------------------------------
384// Auto close bracket
385// ---------------------------------------------------------------------------
386
387/// Returns the closing counterpart for an opening bracket or quote character.
388///
389/// Supports `()`, `[]`, `{}`, `""`, `''`.
390pub fn auto_close_bracket(ch: char) -> Option<char> {
391    match ch {
392        '(' => Some(')'),
393        '[' => Some(']'),
394        '{' => Some('}'),
395        '"' => Some('"'),
396        '\'' => Some('\''),
397        _ => None,
398    }
399}
400
401fn gcd_u32(a: u32, b: u32) -> u32 {
402    if b == 0 {
403        a
404    } else {
405        gcd_u32(b, a % b)
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    use neco_decor::Decoration;
414    use neco_textpatch::apply_patches;
415
416    #[test]
417    fn new_exposes_text_and_line_index() {
418        let buffer = EditorBuffer::new("alpha\nbeta".to_string());
419
420        assert_eq!(buffer.text(), "alpha\nbeta");
421        assert_eq!(buffer.line_index().line_count(), 2);
422        assert_eq!(buffer.line_index().text_len(), 10);
423    }
424
425    #[test]
426    fn apply_patches_updates_text_and_returns_single_range_change() {
427        let mut buffer = EditorBuffer::new("hello world".to_string());
428        let patches = [TextPatch::replace(6, 11, "rust").unwrap()];
429
430        let changes = buffer.apply_patches(&patches).unwrap();
431
432        assert_eq!(buffer.text(), "hello rust");
433        assert_eq!(changes, vec![RangeChange::new(6, 11, 10)]);
434        assert_eq!(buffer.line_index().text_len(), 10);
435    }
436
437    #[test]
438    fn apply_patches_uses_cumulative_delta_for_following_changes() {
439        let mut buffer = EditorBuffer::new("abcdef".to_string());
440        let patches = [
441            TextPatch::replace(1, 3, "WXYZ").unwrap(),
442            TextPatch::replace(4, 6, "Q").unwrap(),
443        ];
444
445        let changes = buffer.apply_patches(&patches).unwrap();
446
447        assert_eq!(buffer.text(), "aWXYZdQ");
448        assert_eq!(
449            changes,
450            vec![RangeChange::new(1, 3, 5), RangeChange::new(6, 8, 7)]
451        );
452    }
453
454    #[test]
455    fn apply_patches_returns_error_for_invalid_patch() {
456        let mut buffer = EditorBuffer::new("abc".to_string());
457        let patches = [TextPatch::replace(4, 4, "x").unwrap()];
458
459        let error = buffer.apply_patches(&patches).unwrap_err();
460
461        assert_eq!(
462            error,
463            TextPatchError::OffsetOutOfBounds { offset: 4, len: 3 }
464        );
465    }
466
467    #[test]
468    fn apply_patches_with_maps_decorations_through_changes() {
469        let mut buffer = EditorBuffer::new("hello world".to_string());
470        let patches = [TextPatch::replace(6, 11, "rust").unwrap()];
471        let mut decorations = DecorationSet::new();
472        decorations.add(Decoration::highlight(6, 11, 1).unwrap());
473
474        buffer
475            .apply_patches_with(&patches, &mut decorations, None, None, None, None)
476            .unwrap();
477
478        let decoration = decorations.iter().next().unwrap().1;
479        assert_eq!(decoration.start(), 6);
480        assert_eq!(decoration.end(), 10);
481    }
482
483    #[test]
484    fn apply_patches_with_updates_wrap_map() {
485        let mut buffer = EditorBuffer::new("ab cd\nef gh".to_string());
486        let patches = [TextPatch::replace(0, 5, "abcd").unwrap()];
487        let policy = WrapPolicy::code();
488        let mut decorations = DecorationSet::new();
489        let mut wrap_map = WrapMap::new(buffer.text().split('\n'), 3, &policy);
490
491        assert_eq!(wrap_map.visual_line_count(0), 2);
492
493        buffer
494            .apply_patches_with(
495                &patches,
496                &mut decorations,
497                Some(&mut wrap_map),
498                Some(&policy),
499                None,
500                None,
501            )
502            .unwrap();
503
504        assert_eq!(wrap_map.visual_line_count(0), 1);
505        assert_eq!(wrap_map.wrap_points(0), &[]);
506    }
507
508    #[test]
509    fn apply_patches_with_records_history_and_undo_restores_original_text() {
510        let mut buffer = EditorBuffer::new("hello world".to_string());
511        let patches = [TextPatch::replace(6, 11, "rust").unwrap()];
512        let mut decorations = DecorationSet::new();
513        let mut history = EditHistory::new(buffer.text());
514
515        buffer
516            .apply_patches_with(
517                &patches,
518                &mut decorations,
519                None,
520                None,
521                Some(&mut history),
522                Some("replace word"),
523            )
524            .unwrap();
525
526        assert_eq!(history.current_label(), "replace word");
527
528        let undo = history.undo().unwrap().remove(0);
529        let inverse = undo.inverse_patches.unwrap();
530        let restored = apply_patches(buffer.text(), &inverse).unwrap();
531
532        assert_eq!(restored, "hello world");
533    }
534
535    #[test]
536    fn apply_patches_with_works_when_all_optional_systems_are_absent() {
537        let mut buffer = EditorBuffer::new("hello".to_string());
538        let patches = [TextPatch::insert(5, "!")];
539        let mut decorations = DecorationSet::new();
540
541        buffer
542            .apply_patches_with(&patches, &mut decorations, None, None, None, None)
543            .unwrap();
544
545        assert_eq!(buffer.text(), "hello!");
546    }
547
548    #[test]
549    fn re_exports_are_available() {
550        let _ = neco_textview::LineIndex::new("text");
551        let _ = neco_textpatch::TextPatch::insert(0, "x");
552        let _ = neco_decor::DecorationSet::new();
553        let _ = neco_diffcore::diff("a", "b");
554        let _ = neco_wrap::WrapPolicy::code();
555        let _ = neco_history::EditHistory::new("");
556        let _ = neco_pathrel::PathPolicy::posix();
557        let _ = neco_filetree::FileTreeNode {
558            name: "a".to_string(),
559            path: "/a".to_string(),
560            kind: neco_filetree::FileTreeNodeKind::File,
561            children: Vec::new(),
562            materialization: neco_filetree::DirectoryMaterialization::Complete,
563            child_count: None,
564        };
565        let _ = neco_watchnorm::RawWatchKind::Create;
566        let _ = neco_tree::Tree::new(0usize);
567        let _ = RangeChange::new(0, 0, 0);
568    }
569
570    #[test]
571    fn detect_indent_tabs() {
572        let buffer = EditorBuffer::new("\tline1\n\t\tline2\nline3\n".to_string());
573        assert_eq!(buffer.detect_indent(10), IndentStyle::Tabs);
574    }
575
576    #[test]
577    fn detect_indent_two_spaces() {
578        let buffer = EditorBuffer::new("def foo\n  bar\n  baz\n    qux\n".to_string());
579        assert_eq!(buffer.detect_indent(10), IndentStyle::Spaces(2));
580    }
581
582    #[test]
583    fn detect_indent_four_spaces() {
584        let buffer = EditorBuffer::new(
585            "fn main() {\n    let x = 1;\n    let y = 2;\n        nested();\n}\n".to_string(),
586        );
587        assert_eq!(buffer.detect_indent(10), IndentStyle::Spaces(4));
588    }
589
590    #[test]
591    fn detect_indent_mixed_prefers_majority() {
592        // More tab-indented lines than space-indented
593        let buffer = EditorBuffer::new("\ta\n\tb\n\tc\n  d\n".to_string());
594        assert_eq!(buffer.detect_indent(10), IndentStyle::Tabs);
595    }
596
597    #[test]
598    fn detect_indent_empty_text() {
599        let buffer = EditorBuffer::new(String::new());
600        assert_eq!(buffer.detect_indent(10), IndentStyle::Spaces(4));
601    }
602
603    #[test]
604    fn detect_indent_no_indentation() {
605        let buffer = EditorBuffer::new("line1\nline2\nline3\n".to_string());
606        assert_eq!(buffer.detect_indent(10), IndentStyle::Spaces(4));
607    }
608
609    #[test]
610    fn detect_indent_respects_sample_lines_limit() {
611        // First 2 lines use tabs, remaining use spaces
612        let buffer = EditorBuffer::new("\ta\n\tb\n  c\n  d\n  e\n  f\n".to_string());
613        assert_eq!(buffer.detect_indent(2), IndentStyle::Tabs);
614    }
615
616    // -- Bracket matching ----------------------------------------------------
617
618    #[test]
619    fn find_matching_bracket_simple_parens() {
620        let text = "(hello)";
621        let pair = find_matching_bracket(text, 0).unwrap();
622        assert_eq!(pair.open(), 0);
623        assert_eq!(pair.close(), 6);
624    }
625
626    #[test]
627    fn find_matching_bracket_from_close() {
628        let text = "(hello)";
629        let pair = find_matching_bracket(text, 6).unwrap();
630        assert_eq!(pair.open(), 0);
631        assert_eq!(pair.close(), 6);
632    }
633
634    #[test]
635    fn find_matching_bracket_nested() {
636        let text = "((a))";
637        let pair = find_matching_bracket(text, 1).unwrap();
638        assert_eq!(pair.open(), 1);
639        assert_eq!(pair.close(), 3);
640        let pair = find_matching_bracket(text, 0).unwrap();
641        assert_eq!(pair.open(), 0);
642        assert_eq!(pair.close(), 4);
643    }
644
645    #[test]
646    fn find_matching_bracket_mixed_types() {
647        let text = "{[()]}";
648        let pair = find_matching_bracket(text, 0).unwrap();
649        assert_eq!(pair.open(), 0);
650        assert_eq!(pair.close(), 5);
651        let pair = find_matching_bracket(text, 1).unwrap();
652        assert_eq!(pair.open(), 1);
653        assert_eq!(pair.close(), 4);
654        let pair = find_matching_bracket(text, 2).unwrap();
655        assert_eq!(pair.open(), 2);
656        assert_eq!(pair.close(), 3);
657    }
658
659    #[test]
660    fn find_matching_bracket_not_on_bracket() {
661        assert!(find_matching_bracket("hello", 0).is_none());
662    }
663
664    #[test]
665    fn find_matching_bracket_unmatched() {
666        assert!(find_matching_bracket("(hello", 0).is_none());
667        assert!(find_matching_bracket("hello)", 5).is_none());
668    }
669
670    #[test]
671    fn find_matching_bracket_empty_text() {
672        assert!(find_matching_bracket("", 0).is_none());
673    }
674
675    // -- Auto-indent ---------------------------------------------------------
676
677    #[test]
678    fn auto_indent_preserves_spaces() {
679        let text = "    hello\n    world";
680        let li = neco_textview::LineIndex::new(text);
681        assert_eq!(auto_indent(text, &li, 0), "    ");
682        assert_eq!(auto_indent(text, &li, 10), "    ");
683    }
684
685    #[test]
686    fn auto_indent_preserves_tabs() {
687        let text = "\thello\n\t\tworld";
688        let li = neco_textview::LineIndex::new(text);
689        assert_eq!(auto_indent(text, &li, 0), "\t");
690        assert_eq!(auto_indent(text, &li, 7), "\t\t");
691    }
692
693    #[test]
694    fn auto_indent_no_indent_returns_empty() {
695        let text = "hello\nworld";
696        let li = neco_textview::LineIndex::new(text);
697        assert_eq!(auto_indent(text, &li, 0), "");
698    }
699
700    #[test]
701    fn auto_indent_empty_text() {
702        let text = "";
703        let li = neco_textview::LineIndex::new(text);
704        assert_eq!(auto_indent(text, &li, 0), "");
705    }
706
707    // -- Auto close bracket --------------------------------------------------
708
709    #[test]
710    fn auto_close_bracket_pairs() {
711        assert_eq!(auto_close_bracket('('), Some(')'));
712        assert_eq!(auto_close_bracket('['), Some(']'));
713        assert_eq!(auto_close_bracket('{'), Some('}'));
714        assert_eq!(auto_close_bracket('"'), Some('"'));
715        assert_eq!(auto_close_bracket('\''), Some('\''));
716    }
717
718    #[test]
719    fn auto_close_bracket_non_bracket() {
720        assert_eq!(auto_close_bracket('a'), None);
721        assert_eq!(auto_close_bracket(')'), None);
722        assert_eq!(auto_close_bracket(']'), None);
723    }
724
725    #[test]
726    fn find_matching_bracket_offset_out_of_bounds() {
727        assert!(find_matching_bracket("()", 5).is_none());
728    }
729}