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#[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 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 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 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 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 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#[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
288fn 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
301fn is_opening_bracket(ch: char) -> bool {
303 matches!(ch, '(' | '[' | '{')
304}
305
306pub 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 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 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
358pub 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
383pub 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 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 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 #[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 #[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 #[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}