1use crate::{
3 Config, MAX_NAME_LEN, UNNAMED_BUFFER,
4 config::ftype_config_for_path_and_first_line,
5 config_handle,
6 dot::{Cur, Dot, Range, TextObject, find::find_forward_wrapping},
7 editor::Action,
8 exec::{Addr, Address},
9 fsys::InputFilter,
10 key::Input,
11 lsp::Coords,
12 syntax::{LineIter, SyntaxState},
13 util::normalize_line_endings,
14};
15use ad_event::Source;
16use std::{
17 cmp::min,
18 fs,
19 io::{self, ErrorKind},
20 path::{Path, PathBuf},
21 sync::{
22 Arc, RwLock,
23 atomic::{AtomicUsize, Ordering},
24 },
25 time::SystemTime,
26};
27use tracing::{debug, error};
28
29mod buffers;
30mod edit;
31mod internal;
32
33use edit::{Edit, EditLog, Kind, Txt};
34
35pub use buffers::BufferId;
36pub(crate) use buffers::Buffers;
37pub use internal::{Chars, GapBuffer, IdxChars, Slice, SliceIter};
38
39pub(crate) const WELCOME_SQUIRREL: &str = r#"+---------------------------------------------------------------------------+
42| > Welcome to the ad text editor! |
43| This is a temporary buffer where you can make notes and execute commands. |
44| You can press the '-' key to open a file from the current directory, or |
45| type :help to view the in-editor help documentation. |
46| |
47| To prevent this message being shown at startup, set the 'show_splash' |
48| property to false in your config file. |
49+---------------------------------------------------------------------------+
50 \ ,, __
51 ("\ ( (
52 -)>\ )/
53 ('( )'
54 -'-'"#;
55pub(crate) const DEFAULT_OUTPUT_BUFFER: &str = "+output";
56const HTTPS: &str = "https://";
57const HTTP: &str = "http://";
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ActionOutcome {
63 SetClipboard(String),
64 SetStatusMessage(String),
65}
66
67#[derive(Default, Debug, Clone, PartialEq, Eq)]
69pub(crate) enum BufferKind {
70 File(PathBuf),
72 Directory(PathBuf),
74 Virtual(String),
76 Output(String),
78 #[default]
80 Unnamed,
81 MiniBuffer,
83}
84
85impl BufferKind {
86 fn display_name(&self) -> String {
87 match self {
88 BufferKind::File(p) => p.display().to_string(),
89 BufferKind::Directory(p) => p.display().to_string(),
90 BufferKind::Virtual(s) => s.clone(),
91 BufferKind::Output(s) => s.clone(),
92 BufferKind::Unnamed => UNNAMED_BUFFER.to_string(),
93 BufferKind::MiniBuffer => "".to_string(),
94 }
95 }
96
97 fn path(&self) -> Option<&Path> {
99 match &self {
100 BufferKind::File(p) => Some(p.as_ref()),
101 _ => None,
102 }
103 }
104
105 fn dir(&self) -> Option<&Path> {
107 match &self {
108 BufferKind::File(p) => p.parent(),
109 BufferKind::Directory(p) => Some(p.as_ref()),
110 BufferKind::Output(s) => Path::new(s).parent(),
111 _ => None,
112 }
113 }
114
115 pub(crate) fn is_file(&self) -> bool {
116 matches!(self, Self::File(_))
117 }
118
119 pub(crate) fn is_dir(&self) -> bool {
120 matches!(self, Self::Directory(_))
121 }
122
123 pub fn output_file_key(&self, cwd: &Path) -> String {
126 let path = self.dir().unwrap_or(cwd);
127 format!("{}/{DEFAULT_OUTPUT_BUFFER}", path.display())
128 }
129
130 fn try_kind_and_content_from_path(path: PathBuf) -> io::Result<(Self, String)> {
131 match path.metadata() {
132 Ok(m) if m.is_dir() => {
133 let mut raw_entries = Vec::new();
134 for entry in path.read_dir()? {
135 let p = entry?.path();
136 let mut s = p.strip_prefix(&path).unwrap_or(&p).display().to_string();
137 if p.metadata().map(|m| m.is_dir()).unwrap_or_default() {
138 s.push('/');
139 }
140 raw_entries.push(s);
141 }
142 raw_entries.sort_unstable();
143
144 let mut raw = format!("{}\n\n..\n", path.display());
145 raw.push_str(&raw_entries.join("\n"));
146
147 Ok((Self::Directory(path), raw))
148 }
149
150 _ => {
151 let raw = match fs::read_to_string(&path) {
152 Ok(contents) => normalize_line_endings(contents),
153 Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
154 Err(e) => return Err(e),
155 };
156
157 Ok((Self::File(path), raw))
158 }
159 }
160 }
161}
162
163#[derive(Debug)]
165pub struct Buffer {
166 pub(crate) id: usize,
167 pub(crate) kind: BufferKind,
168 pub(crate) dot: Dot,
169 pub(crate) xdot: Dot,
170 pub(crate) txt: GapBuffer,
171 pub(crate) cached_rx: usize,
172 pub(crate) last_save: SystemTime,
173 pub(crate) dirty: bool,
174 pub(crate) changed_since_last_render: bool,
175 pub(crate) input_filter: Option<InputFilter>,
176 pub(crate) syntax_state: Option<SyntaxState>,
177 config: Arc<RwLock<Config>>,
178 version: AtomicUsize,
179 edit_log: EditLog,
180}
181
182impl Buffer {
183 pub fn new_from_canonical_file_path(
185 id: usize,
186 path: PathBuf,
187 config: Arc<RwLock<Config>>,
188 ) -> io::Result<Self> {
189 let (kind, raw) = BufferKind::try_kind_and_content_from_path(path.clone())?;
190 let mut b = Self {
191 id,
192 kind,
193 dot: Dot::default(),
194 xdot: Dot::default(),
195 txt: GapBuffer::from(raw),
196 cached_rx: 0,
197 last_save: SystemTime::now(),
198 dirty: false,
199 changed_since_last_render: false,
200 input_filter: None,
201 syntax_state: None,
202 config,
203 version: AtomicUsize::new(1),
204 edit_log: EditLog::default(),
205 };
206
207 b.try_set_ts_state();
208
209 Ok(b)
210 }
211
212 pub fn new_unnamed(id: usize, content: impl Into<String>, config: Arc<RwLock<Config>>) -> Self {
214 Self {
215 id,
216 kind: BufferKind::Unnamed,
217 dot: Dot::default(),
218 xdot: Dot::default(),
219 txt: GapBuffer::from(normalize_line_endings(content.into())),
220 cached_rx: 0,
221 last_save: SystemTime::now(),
222 dirty: false,
223 changed_since_last_render: false,
224 input_filter: None,
225 syntax_state: None,
226 config,
227 version: AtomicUsize::new(1),
228 edit_log: EditLog::default(),
229 }
230 }
231
232 pub fn new_virtual(
237 id: usize,
238 name: impl Into<String>,
239 content: impl Into<String>,
240 config: Arc<RwLock<Config>>,
241 ) -> Self {
242 let mut content = normalize_line_endings(content.into());
243 if content.ends_with('\n') {
244 content.pop();
245 }
246
247 Self {
248 id,
249 kind: BufferKind::Virtual(name.into()),
250 dot: Dot::default(),
251 xdot: Dot::default(),
252 txt: GapBuffer::from(content),
253 cached_rx: 0,
254 last_save: SystemTime::now(),
255 dirty: false,
256 changed_since_last_render: false,
257 input_filter: None,
258 syntax_state: None,
259 config,
260 version: AtomicUsize::new(1),
261 edit_log: EditLog::default(),
262 }
263 }
264
265 pub(super) fn new_output(
268 id: usize,
269 name: String,
270 content: String,
271 config: Arc<RwLock<Config>>,
272 ) -> Self {
273 Self {
274 id,
275 kind: BufferKind::Output(name),
276 dot: Dot::default(),
277 xdot: Dot::default(),
278 txt: GapBuffer::from(normalize_line_endings(content)),
279 cached_rx: 0,
280 last_save: SystemTime::now(),
281 dirty: false,
282 changed_since_last_render: false,
283 input_filter: None,
284 syntax_state: None,
285 config,
286 version: AtomicUsize::new(1),
287 edit_log: EditLog::default(),
288 }
289 }
290
291 fn try_set_ts_state(&mut self) {
294 self.syntax_state = None;
295 if let Some(lang) = self.configured_filetype() {
296 let cfg = config_handle!(self);
297 match SyntaxState::try_new(&lang, &self.txt, &cfg) {
298 Ok(state) => self.syntax_state = Some(state),
299 Err(msg) => error!("unable to initialise syntax state: {msg}"),
300 }
301 }
302 }
303
304 pub(crate) fn next_edit_version(&self) -> usize {
305 self.version.fetch_add(1, Ordering::Relaxed)
306 }
307
308 pub(crate) fn state_changed_on_disk(&self) -> Result<bool, String> {
309 fn inner(p: &Path, last_save: SystemTime) -> io::Result<bool> {
310 let modified = p.metadata()?.modified()?;
311 Ok(modified > last_save)
312 }
313
314 let path = match &self.kind {
315 BufferKind::File(p) => p,
316 _ => return Ok(false),
317 };
318
319 match inner(path, self.last_save) {
320 Ok(modified) => Ok(modified),
321 Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
322 Err(e) => Err(format!("Error checking file state: {e}")),
323 }
324 }
325
326 pub(crate) fn set_filename<P: AsRef<Path>>(&mut self, path: P) -> Option<ActionOutcome> {
329 let path = match path.as_ref().canonicalize() {
330 Ok(p) => p,
331 Err(e) if e.kind() == ErrorKind::NotFound => path.as_ref().to_path_buf(),
332 Err(e) => {
333 return Some(ActionOutcome::SetStatusMessage(format!(
334 "invalid file path: {e}"
335 )));
336 }
337 };
338
339 let kind = if path.is_dir() {
340 BufferKind::Directory(path)
341 } else {
342 BufferKind::File(path)
343 };
344
345 self.kind = kind;
346 self.try_set_ts_state();
347 self.changed_since_last_render = true;
348
349 None
350 }
351
352 pub(crate) fn save_to_disk_at(
353 &mut self,
354 path: impl AsRef<Path>,
355 force: bool,
356 ) -> Result<String, String> {
357 if !self.has_trailing_newline() {
358 self.insert_char(TextObject::BufferEnd.as_dot(self), '\n', None);
359 }
360
361 if !self.dirty {
362 return Err("Nothing to save".to_string());
363 }
364
365 if !force {
366 match self.state_changed_on_disk() {
367 Ok(false) => (),
368 Ok(true) => return Err("File modified on disk, use :w! to force".to_string()),
369 Err(s) => return Err(s),
370 }
371 }
372
373 let path = path.as_ref();
374 let n_lines = self.len_lines();
375 let contents = self.txt.make_contiguous();
376 let display_path = match path.canonicalize() {
377 Ok(cp) => cp.display().to_string(),
378 Err(_) => path.display().to_string(),
379 };
380 let n_bytes = contents.len();
381
382 #[cfg(not(feature = "fuzz"))]
383 {
384 match fs::write(path, contents) {
385 Ok(_) => {
386 self.dirty = false;
387 self.last_save = SystemTime::now();
388 Ok(format!("\"{display_path}\" {n_lines}L {n_bytes}B written"))
389 }
390 Err(e) => Err(format!("Unable to save buffer: {e}")),
391 }
392 }
393
394 #[cfg(feature = "fuzz")]
395 {
396 self.dirty = false;
397 self.last_save = SystemTime::now();
398 Ok(format!("\"{display_path}\" {n_lines}L {n_bytes}B written"))
399 }
400 }
401
402 pub(super) fn reload_from_disk(&mut self) -> String {
403 let path = match &self.kind {
404 BufferKind::File(p) | BufferKind::Directory(p) => p,
405 _ => return "Buffer is not backed by a file on disk".to_string(),
406 };
407
408 debug!(id=%self.id, path=%path.as_os_str().to_string_lossy(), "reloading buffer state from disk");
409 let raw = match BufferKind::try_kind_and_content_from_path(path.to_path_buf()) {
410 Ok((_, raw)) => raw,
411 Err(e) => return format!("Error reloading buffer: {e}"),
412 };
413
414 let n_chars = raw.len();
415 self.txt = GapBuffer::from(raw);
416 self.dot.clamp_idx(n_chars);
417 self.xdot.clamp_idx(n_chars);
418 self.edit_log.clear();
419 self.dirty = false;
420 self.changed_since_last_render = true;
421 self.last_save = SystemTime::now();
422
423 let n_lines = self.txt.len_lines();
424 let n_bytes = self.txt.len();
425 debug!(%n_bytes, "reloaded buffer content");
426
427 let display_path = match path.canonicalize() {
428 Ok(cp) => cp.display().to_string(),
429 Err(_) => path.display().to_string(),
430 };
431
432 format!("\"{display_path}\" {n_lines}L {n_bytes}B loaded")
433 }
434
435 pub(super) fn new_minibuffer(config: Arc<RwLock<Config>>) -> Self {
436 Self {
437 id: usize::MAX,
438 kind: BufferKind::MiniBuffer,
439 dot: Default::default(),
440 xdot: Default::default(),
441 txt: GapBuffer::from(""),
442 cached_rx: 0,
443 last_save: SystemTime::now(),
444 dirty: false,
445 changed_since_last_render: false,
446 input_filter: None,
447 syntax_state: None,
448 config,
449 version: AtomicUsize::new(1),
450 edit_log: Default::default(),
451 }
452 }
453
454 pub fn display_name(&self) -> String {
456 let s = self.kind.display_name();
457
458 s[0..min(MAX_NAME_LEN, s.len())].to_string()
459 }
460
461 pub fn full_name(&self) -> &str {
463 match &self.kind {
464 BufferKind::File(p) => p.to_str().expect("valid unicode"),
465 BufferKind::Directory(p) => p.to_str().expect("valid unicode"),
466 BufferKind::Virtual(s) => s,
467 BufferKind::Output(s) => s,
468 BufferKind::Unnamed => UNNAMED_BUFFER,
469 BufferKind::MiniBuffer => "*mini-buffer*",
470 }
471 }
472
473 pub fn dir(&self) -> Option<&Path> {
475 self.kind.dir()
476 }
477
478 pub fn path(&self) -> Option<&Path> {
480 self.kind.path()
481 }
482
483 pub fn output_file_key(&self, cwd: &Path) -> String {
486 self.kind.output_file_key(cwd)
487 }
488
489 pub fn is_unnamed(&self) -> bool {
491 self.kind == BufferKind::Unnamed
492 }
493
494 pub fn has_trailing_newline(&self) -> bool {
501 self.txt.has_trailing_newline()
502 }
503
504 pub fn configured_filetype(&self) -> Option<String> {
506 let lang_configs = &config_handle!(self).filetypes;
507 let first_line = self.line(0).map(|l| l.to_string()).unwrap_or_default();
508
509 self.path()
510 .and_then(|path| ftype_config_for_path_and_first_line(path, &first_line, lang_configs))
511 .map(|(lang, _)| lang.clone())
512 }
513
514 pub fn str_contents(&self) -> String {
516 self.txt.to_string()
517 }
518
519 pub(crate) fn pretty_print_ts_tree(&self) -> Option<String> {
520 self.syntax_state
521 .as_ref()
522 .and_then(|st| st.pretty_print_tree())
523 }
524
525 pub(crate) fn string_lines(&self) -> Vec<String> {
526 self.txt
527 .iter_lines()
528 .map(|l| {
529 let mut s = l.to_string();
530 if s.ends_with('\n') {
531 s.pop();
532 }
533 s
534 })
535 .collect()
536 }
537
538 pub fn update_ts_state(&mut self, from: usize, n_rows: usize) {
539 if let Some(ts) = self.syntax_state.as_mut() {
540 ts.update(&self.txt, from, n_rows);
541 }
542 }
543
544 pub fn iter_tokenized_lines_from(
545 &self,
546 line: usize,
547 load_exec_range: Option<(bool, Range)>,
548 ) -> LineIter<'_> {
549 match self.syntax_state.as_ref() {
550 Some(ts) => {
551 ts.iter_tokenized_lines_from(line, &self.txt, self.dot.as_range(), load_exec_range)
552 }
553 None => LineIter::new(
554 line,
555 &self.txt,
556 self.dot.as_range(),
557 load_exec_range,
558 &[],
559 &[],
560 ),
561 }
562 }
563
564 pub fn dot_contents(&self) -> String {
566 self.dot.content(self)
567 }
568
569 pub fn addr(&self) -> String {
571 self.dot.addr(self)
572 }
573
574 pub fn xdot_contents(&self) -> String {
578 self.xdot.content(self)
579 }
580
581 pub fn xaddr(&self) -> String {
585 self.xdot.addr(self)
586 }
587
588 pub(crate) fn tabstop(&self) -> usize {
589 config_handle!(self).tabstop
590 }
591
592 #[inline]
594 pub fn len_lines(&self) -> usize {
595 self.txt.len_lines()
596 }
597
598 #[inline]
600 pub fn len_chars(&self) -> usize {
601 self.txt.len_chars()
602 }
603
604 #[inline]
610 pub fn is_empty(&self) -> bool {
611 self.txt.len_chars() == 0
612 }
613
614 pub fn clear(&mut self) {
616 self.handle_action(Action::DotSet(TextObject::BufferStart, 1), Source::Fsys);
617 self.handle_action(
618 Action::DotExtendForward(TextObject::BufferEnd, 1),
619 Source::Fsys,
620 );
621 self.handle_action(Action::Delete, Source::Fsys);
622 self.xdot.clamp_idx(self.txt.len_chars());
623 }
624
625 pub(crate) fn debug_edit_log(&self) -> Vec<String> {
626 self.edit_log.debug_edits(self)
627 }
628
629 pub(crate) fn x_from_rx(&self, y: usize) -> usize {
630 self.x_from_provided_rx(y, self.cached_rx)
631 }
632
633 pub(crate) fn x_from_provided_rx(&self, y: usize, buf_rx: usize) -> usize {
634 let tabstop = config_handle!(self).tabstop;
635 if self.is_empty() {
636 return 0;
637 }
638
639 let mut rx = 0;
640 let mut cx = 0;
641
642 for c in self.txt.line(y).chars() {
643 if c == '\n' {
644 break;
645 }
646
647 if c == '\t' {
648 rx += (tabstop - 1) - (rx % tabstop);
649 }
650
651 rx += unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
657
658 if rx > buf_rx {
659 break;
660 }
661 cx += 1;
662 }
663
664 cx
665 }
666
667 pub fn line(&self, y: usize) -> Option<Slice<'_>> {
669 if y >= self.len_lines() {
670 None
671 } else {
672 Some(self.txt.line(y))
673 }
674 }
675
676 pub(crate) fn iter_between_chars(
681 &self,
682 char_from: usize,
683 char_to: usize,
684 ) -> impl Iterator<Item = (usize, char)> {
685 self.txt
686 .slice(char_from, char_to)
687 .indexed_chars(char_from, false)
688 }
689
690 pub(crate) fn rev_iter_between_chars(
695 &self,
696 char_from: usize,
697 char_to: usize,
698 ) -> impl Iterator<Item = (usize, char)> {
699 self.txt
700 .slice(char_to, char_from)
701 .indexed_chars(char_to, true)
702 }
703
704 pub(crate) fn try_expand_delimited(&mut self) {
707 let current_index = match self.dot {
708 Dot::Cur { c: Cur { idx } } => idx,
709 Dot::Range { .. } => return,
710 };
711
712 let prev = if current_index == 0 {
713 None
714 } else {
715 self.txt.get_char(current_index - 1)
716 };
717 let next = self.txt.get_char(current_index + 1);
718
719 let (l, r) = match (prev, next) {
720 (Some('\n'), _) | (_, Some('\n')) => ('\n', '\n'),
721 (Some('('), _) | (_, Some(')')) => ('(', ')'),
722 (Some('['), _) | (_, Some(']')) => ('[', ']'),
723 (Some('{'), _) | (_, Some('}')) => ('{', '}'),
724 (Some('<'), _) | (_, Some('>')) => ('<', '>'),
725 (Some('"'), _) | (_, Some('"')) => ('"', '"'),
726 (Some('\''), _) | (_, Some('\'')) => ('\'', '\''),
727 (Some(' '), _) | (_, Some(' ')) => (' ', ' '),
728
729 _ => return self.expand_cur_dot(),
730 };
731
732 self.set_dot(TextObject::Delimited(l, r), 1);
733 self.changed_since_last_render = true;
734 }
735
736 pub(crate) fn expand_cur_dot(&mut self) {
741 let current_index = match self.dot {
742 Dot::Cur { c: Cur { idx } } => idx,
743 Dot::Range { .. } => return,
744 };
745 self.changed_since_last_render = true;
746
747 if let Some(dot) = self.try_expand_known(current_index) {
748 self.dot = dot;
749 return;
750 }
751
752 let (mut from, mut to) = (current_index, current_index);
754 for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) {
755 if !(ch == '_' || ch.is_alphanumeric()) {
756 break;
757 }
758 to = i;
759 }
760
761 for (i, ch) in self.rev_iter_between_chars(current_index, 0) {
762 if !(ch == '_' || ch.is_alphanumeric()) {
763 break;
764 }
765 from = i;
766 }
767
768 self.dot = Dot::from_char_indices(from, to);
769 }
770
771 fn try_expand_known(&self, current_index: usize) -> Option<Dot> {
777 let (mut from, mut to) = (current_index, current_index);
778 let mut colon: Option<usize> = None;
779 let n_chars = self.txt.len_chars();
780
781 let is_file_char = |ch: char| ch.is_alphanumeric() || "._-+/:@".contains(ch);
782 let is_addr_char = |ch: char| "+-/$.#,;?".contains(ch);
783 let is_url_char = |ch: char| "?&=".contains(ch);
784 let has_url_prefix = |i: usize| {
785 let http = i > 4 && i + 3 <= n_chars && self.txt.slice(i - 4, i + 3) == HTTP;
786 let https = i > 5 && i + 3 <= n_chars && self.txt.slice(i - 5, i + 3) == HTTPS;
787
788 http || https
789 };
790
791 for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) {
794 if !is_file_char(ch) {
795 break;
796 }
797 if ch == ':' && !has_url_prefix(i) {
798 colon = Some(i);
799 break;
800 }
801 to = i;
802 }
803
804 for (i, ch) in self.rev_iter_between_chars(current_index, 0) {
805 if !(is_file_char(ch) || is_url_char(ch) || is_addr_char(ch)) {
806 break;
807 }
808 if colon.is_none() && ch == ':' && !has_url_prefix(i) {
809 colon = Some(i);
810 }
811 from = i;
812 }
813
814 if let Some(ix) = colon {
816 to = ix;
817 for (_, ch) in self.iter_between_chars(ix + 1, self.txt.len_chars()) {
818 if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
819 break;
820 }
821 to += 1;
822 }
823 }
824
825 let dot_content = self.txt.slice(from, to + 1).to_string();
826
827 if dot_content.starts_with(HTTP) || dot_content.starts_with(HTTPS) {
829 if to < self.txt.len_chars() {
830 for (_, ch) in self.iter_between_chars(to + 1, self.txt.len_chars()) {
831 if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
832 break;
833 }
834 to += 1;
835 }
836 }
837
838 if dot_content.ends_with('.') {
839 to -= 1;
840 }
841
842 return Some(Dot::from_char_indices(from, to));
843 }
844
845 let dot = Dot::from_char_indices(from, to);
846
847 let fname = match dot_content.split_once(':') {
849 Some((fname, _)) => fname,
850 None => &dot_content,
851 };
852
853 let path = Path::new(fname);
854 if path.is_absolute() && path.exists() {
855 return Some(dot);
856 } else if let Some(dir) = self.dir()
857 && dir.join(path).exists()
858 {
859 return Some(dot);
860 }
861
862 None
864 }
865
866 pub(crate) fn sign_col_dims(&self) -> (usize, usize) {
867 let w_lnum = n_digits(self.len_lines());
868 let w_sgncol = w_lnum + 2;
869
870 (w_lnum, w_sgncol)
871 }
872
873 pub(crate) fn append(&mut self, s: String, source: Source) {
874 let dot = self.dot;
875 self.set_dot(TextObject::BufferEnd, 1);
876 self.handle_action(Action::InsertString { s }, source);
877 self.dot = dot;
878 self.dot.clamp_idx(self.txt.len_chars());
879 self.xdot.clamp_idx(self.txt.len_chars());
880 self.changed_since_last_render = true;
881 }
882
883 pub fn handle_action(&mut self, a: Action, source: Source) -> Option<ActionOutcome> {
885 match a {
886 Action::Delete => {
887 let (c, deleted) = self.delete_dot(self.dot, Some(source));
888 self.dot = Dot::Cur { c };
889 self.dot.clamp_idx(self.txt.len_chars());
890 self.xdot.clamp_idx(self.txt.len_chars());
891 return deleted.map(ActionOutcome::SetClipboard);
892 }
893 Action::InsertChar { c } => {
894 let (c, _) = self.insert_char(self.dot, c, Some(source));
895 self.dot = Dot::Cur { c };
896 self.dot.clamp_idx(self.txt.len_chars());
897 self.xdot.clamp_idx(self.txt.len_chars());
898 return None;
899 }
900 Action::InsertString { s } => {
901 let (c, _) = self.insert_string(self.dot, s, Some(source));
902 self.dot = Dot::Cur { c };
903 self.dot.clamp_idx(self.txt.len_chars());
904 self.xdot.clamp_idx(self.txt.len_chars());
905 return None;
906 }
907
908 Action::Redo => return self.redo(),
909 Action::Undo => return self.undo(),
910
911 Action::DotCollapseFirst => self.collapse_dot(true),
912 Action::DotCollapseLast => self.collapse_dot(false),
913 Action::DotExtendBackward(tobj, count) => self.extend_dot_backward(tobj, count),
914 Action::DotExtendForward(tobj, count) => self.extend_dot_forward(tobj, count),
915 Action::DotFlip => self.flip_dot(),
916 Action::DotSet(t, count) => self.set_dot(t, count),
917 Action::DotSetFromCoords { coords } => self.set_dot_from_coords(coords),
918
919 Action::XDotSetFromCoords { coords } => self.set_xdot_from_coords(coords),
920 Action::XInsertString { s } => self.insert_xdot(s),
921
922 Action::RenameActiveBuffer { name } => return self.set_filename(name),
923 Action::RawInput { i } => return self.handle_raw_input(i),
924
925 _ => (),
926 }
927
928 None
929 }
930
931 fn handle_raw_input(&mut self, k: Input) -> Option<ActionOutcome> {
932 let (match_indent, expand_tab, tabstop) = {
933 let cfg = config_handle!(self);
934 (cfg.match_indent, cfg.expand_tab, cfg.tabstop)
935 };
936
937 match k {
938 Input::Return => {
939 let mut s = "\n".to_string();
940 if match_indent {
941 let cur = self.dot.first_cur();
942 let y = self.txt.char_to_line(cur.idx);
943 let line = self.txt.line(y).to_string();
944 s.push_str(
945 &line
946 .find(|c: char| !c.is_whitespace())
947 .map(|ix| line.split_at(ix).0.to_string())
948 .unwrap_or_default(),
949 );
950 }
951
952 let c = self.insert_string(self.dot, s, Some(Source::Keyboard)).0;
953
954 self.dot = Dot::Cur { c };
955 return None;
956 }
957
958 Input::Tab => {
959 let (c, _) = if expand_tab {
960 self.insert_string(self.dot, " ".repeat(tabstop), Some(Source::Keyboard))
961 } else {
962 self.insert_char(self.dot, '\t', Some(Source::Keyboard))
963 };
964
965 self.dot = Dot::Cur { c };
966 return None;
967 }
968
969 Input::Char(ch) => {
970 let (c, _) = self.insert_char(self.dot, ch, Some(Source::Keyboard));
971 self.dot = Dot::Cur { c };
972 return None;
973 }
974
975 Input::Arrow(arr) => self.set_dot(TextObject::Arr(arr), 1),
976
977 _ => return None,
978 }
979
980 self.changed_since_last_render = true;
981 None
982 }
983
984 pub(crate) fn set_dot(&mut self, t: TextObject, n: usize) {
986 for _ in 0..n {
987 t.set_dot(self);
988 }
989 self.dot.clamp_idx(self.txt.len_chars());
990 self.xdot.clamp_idx(self.txt.len_chars());
991 self.changed_since_last_render = true;
992 }
993
994 pub fn set_dot_from_cursor(&mut self, idx: usize) {
996 self.dot = Cur::new(idx).into();
997 self.dot.clamp_idx(self.txt.len_chars());
998 self.changed_since_last_render = true;
999 }
1000
1001 pub fn set_dot_from_range(&mut self, from: usize, to: usize) {
1003 self.dot = Dot::from(Range::from_cursors(Cur::new(from), Cur::new(to), false))
1004 .collapse_null_range();
1005 self.dot.clamp_idx(self.txt.len_chars());
1006 self.changed_since_last_render = true;
1007 }
1008
1009 fn set_dot_from_coords(&mut self, coords: Coords) {
1010 let addr: Addr = coords.as_addr(self);
1011 self.dot = self.map_addr(&addr);
1012 self.dot.clamp_idx(self.txt.len_chars());
1013 self.xdot.clamp_idx(self.txt.len_chars());
1014 self.changed_since_last_render = true;
1015 }
1016
1017 fn set_xdot_from_coords(&mut self, coords: Coords) {
1018 let addr: Addr = coords.as_addr(self);
1019 self.xdot = self.map_addr(&addr);
1020 self.xdot.clamp_idx(self.txt.len_chars());
1021 }
1022
1023 fn extend_dot_forward(&mut self, t: TextObject, n: usize) {
1025 for _ in 0..n {
1026 t.extend_dot_forward(self);
1027 }
1028 self.dot.clamp_idx(self.txt.len_chars());
1029 self.xdot.clamp_idx(self.txt.len_chars());
1030 self.changed_since_last_render = true;
1031 }
1032
1033 fn extend_dot_backward(&mut self, t: TextObject, n: usize) {
1035 for _ in 0..n {
1036 t.extend_dot_backward(self);
1037 }
1038 self.dot.clamp_idx(self.txt.len_chars());
1039 self.xdot.clamp_idx(self.txt.len_chars());
1040 self.changed_since_last_render = true;
1041 }
1042
1043 fn collapse_dot(&mut self, first: bool) {
1044 self.dot = if first {
1045 self.dot.collapse_to_first_cur()
1046 } else {
1047 self.dot.collapse_to_last_cur()
1048 };
1049
1050 self.changed_since_last_render = true;
1051 }
1052
1053 fn flip_dot(&mut self) {
1054 self.dot.flip();
1055 self.changed_since_last_render = true;
1056 }
1057
1058 pub(crate) fn new_edit_log_transaction(&mut self) {
1059 self.edit_log.new_transaction()
1060 }
1061
1062 fn undo(&mut self) -> Option<ActionOutcome> {
1063 match self.edit_log.undo() {
1064 Some(edits) => {
1065 self.edit_log.paused = true;
1066 for edit in edits.into_iter() {
1067 self.apply_edit(edit);
1068 }
1069 self.edit_log.paused = false;
1070 self.dirty = !self.edit_log.is_empty();
1071 self.changed_since_last_render = true;
1072 None
1073 }
1074 None => Some(ActionOutcome::SetStatusMessage(
1075 "Nothing to undo".to_string(),
1076 )),
1077 }
1078 }
1079
1080 fn redo(&mut self) -> Option<ActionOutcome> {
1081 match self.edit_log.redo() {
1082 Some(edits) => {
1083 self.edit_log.paused = true;
1084 for edit in edits.into_iter() {
1085 self.apply_edit(edit);
1086 }
1087 self.edit_log.paused = false;
1088 self.dirty = true;
1089 self.changed_since_last_render = true;
1090 None
1091 }
1092 None => Some(ActionOutcome::SetStatusMessage(
1093 "Nothing to redo".to_string(),
1094 )),
1095 }
1096 }
1097
1098 fn apply_edit(&mut self, Edit { kind, cur, txt }: Edit) {
1099 let new_cur = match (kind, txt) {
1100 (Kind::Insert, Txt::Char(c)) => self.insert_char(Dot::Cur { c: cur }, c, None).0,
1101 (Kind::Insert, Txt::String(s)) => self.insert_string(Dot::Cur { c: cur }, s, None).0,
1102 (Kind::Delete, Txt::Char(_)) => self.delete_dot(Dot::Cur { c: cur }, None).0,
1103 (Kind::Delete, Txt::String(s)) => {
1104 let start_idx = cur.idx;
1105 let end_idx = (start_idx + s.chars().count()).saturating_sub(1);
1106 let end = Cur { idx: end_idx };
1107 self.delete_dot(
1108 Dot::Range {
1109 r: Range::from_cursors(cur, end, true),
1110 }
1111 .collapse_null_range(),
1112 None,
1113 )
1114 .0
1115 }
1116 };
1117
1118 self.dot = Dot::Cur { c: new_cur };
1119 }
1120
1121 fn mark_dirty(&mut self) {
1124 self.dirty = self.kind.is_file();
1125 }
1126
1127 pub(crate) fn notify_load(&self, source: Source) -> bool {
1129 match self.input_filter.as_ref() {
1130 Some(f) => {
1131 let (ch_from, ch_to) = self.dot.as_char_indices();
1132 let txt = self.dot.content(self);
1133 f.notify_load(source, ch_from, ch_to, &txt);
1134 true
1135 }
1136 None => false,
1137 }
1138 }
1139
1140 pub(crate) fn notify_execute(&self, source: Source, arg: Option<(Range, String)>) -> bool {
1142 match self.input_filter.as_ref() {
1143 Some(f) => {
1144 let (ch_from, ch_to) = self.dot.as_char_indices();
1145 let txt = self.dot.content(self);
1146 f.notify_execute(source, ch_from, ch_to, &txt, arg);
1147 true
1148 }
1149 None => false,
1150 }
1151 }
1152
1153 fn insert_char(&mut self, dot: Dot, ch: char, source: Option<Source>) -> (Cur, Option<String>) {
1154 let ch = if ch == '\r' { '\n' } else { ch };
1155 let mut have_prepared_edit = false;
1156 if let Dot::Range { r } = &dot
1157 && r.start.idx != r.end.idx
1158 && let Some(s) = self.syntax_state.as_mut()
1159 {
1160 s.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1161 have_prepared_edit = true;
1162 }
1163
1164 let (cur, deleted) = match dot {
1165 Dot::Cur { c } => (c, None),
1166 Dot::Range { r } => self.delete_range(r, source),
1167 };
1168
1169 if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1170 ts.apply_prepared_edit(&self.txt);
1171 }
1172
1173 let idx = cur.idx;
1174 if let Some(s) = self.syntax_state.as_mut() {
1175 s.prepare_insert_char(idx, ch, &self.txt);
1176 }
1177
1178 self.txt.insert_char(idx, ch);
1179
1180 if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1181 f.notify_insert(source, idx, idx + 1, &ch.to_string());
1182 }
1183
1184 self.edit_log.insert_char(cur, ch);
1185 if let Some(ts) = self.syntax_state.as_mut() {
1186 ts.apply_prepared_edit(&self.txt);
1187 }
1188
1189 self.mark_dirty();
1190 self.changed_since_last_render = true;
1191
1192 (Cur { idx: idx + 1 }, deleted)
1193 }
1194
1195 fn insert_string(
1196 &mut self,
1197 dot: Dot,
1198 s: String,
1199 source: Option<Source>,
1200 ) -> (Cur, Option<String>) {
1201 let s = normalize_line_endings(s);
1202 let len = s.chars().count();
1203 let mut have_prepared_edit = false;
1204
1205 if let Dot::Range { r } = &dot
1206 && r.start.idx != r.end.idx
1207 && let Some(ts) = self.syntax_state.as_mut()
1208 {
1209 ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1210 have_prepared_edit = true;
1211 }
1212
1213 let (mut cur, deleted) = match dot {
1214 Dot::Cur { c } => (c, None),
1215 Dot::Range { r } => self.delete_range(r, source),
1216 };
1217
1218 if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1219 ts.apply_prepared_edit(&self.txt);
1220 }
1221
1222 let idx = cur.idx;
1223 have_prepared_edit = false;
1224 if !s.is_empty()
1225 && let Some(ts) = self.syntax_state.as_mut()
1226 {
1227 ts.prepare_insert_string(idx, &s, &self.txt);
1228 have_prepared_edit = true;
1229 }
1230
1231 if !s.is_empty() {
1235 self.txt.insert_str(idx, &s);
1236
1237 if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1238 f.notify_insert(source, idx, idx + len, &s);
1239 }
1240
1241 self.edit_log.insert_string(cur, s);
1242 cur.idx += len;
1243 }
1244
1245 if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1246 ts.apply_prepared_edit(&self.txt);
1247 }
1248
1249 self.mark_dirty();
1250 self.changed_since_last_render = true;
1251
1252 (cur, deleted)
1253 }
1254
1255 fn delete_dot(&mut self, dot: Dot, source: Option<Source>) -> (Cur, Option<String>) {
1256 let mut have_prepared_edit = false;
1257
1258 if let Some(ts) = self.syntax_state.as_mut() {
1259 match &dot {
1260 Dot::Cur { c } if c.idx < self.txt.len_chars() => {
1261 ts.prepare_delete_char(c.idx, &self.txt);
1262 have_prepared_edit = true;
1263 }
1264 Dot::Range { r } if r.start.idx != r.end.idx => {
1265 ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1266 have_prepared_edit = true;
1267 }
1268 _ => (),
1269 }
1270 }
1271
1272 let (cur, deleted) = match dot {
1273 Dot::Cur { c } => (self.delete_cur(c, source), None),
1274 Dot::Range { r } => self.delete_range(r, source),
1275 };
1276
1277 if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1278 ts.apply_prepared_edit(&self.txt);
1279 }
1280
1281 (cur, deleted)
1282 }
1283
1284 fn delete_cur(&mut self, cur: Cur, source: Option<Source>) -> Cur {
1285 let idx = cur.idx;
1286 if idx < self.txt.len_chars() {
1287 let ch = self.txt.char(idx);
1288 self.txt.remove_char(idx);
1289
1290 if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1291 f.notify_delete(source, idx, idx + 1);
1292 }
1293
1294 self.edit_log.delete_char(cur, ch);
1295 self.mark_dirty();
1296 self.changed_since_last_render = true;
1297 }
1298
1299 cur
1300 }
1301
1302 fn delete_range(&mut self, r: Range, source: Option<Source>) -> (Cur, Option<String>) {
1303 let (from, to) = (r.start.idx, min(r.end.idx + 1, self.txt.len_chars()));
1304 let is_single_char = r.start.idx == r.end.idx;
1305
1306 let s = self.txt.slice(from, to).to_string();
1307 self.txt.remove_range(from, to);
1308
1309 if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1310 f.notify_delete(source, from, to);
1311 }
1312
1313 self.edit_log.delete_string(r.start, s.clone());
1314 self.mark_dirty();
1315 self.changed_since_last_render = true;
1316
1317 let deleted = if is_single_char { None } else { Some(s) };
1318
1319 (r.start, deleted)
1320 }
1321
1322 pub(crate) fn find_forward(&mut self, s: &str) {
1323 if let Some(dot) = find_forward_wrapping(&s, self) {
1324 self.dot = dot;
1325 self.changed_since_last_render = true;
1326 }
1327 }
1328
1329 pub(crate) fn insert_xdot(&mut self, s: String) {
1344 let mut offset = s.chars().count() as isize;
1345
1346 if self.dot.as_range().intersects_range(&self.xdot.as_range()) {
1347 if self.xdot.first_cur() > self.dot.first_cur()
1348 && self.xdot.last_cur() >= self.dot.last_cur()
1349 {
1350 offset = -1;
1354 }
1355
1356 self.dot = self.xdot.collapse_to_first_cur();
1357 } else if self.xdot.first_cur() > self.dot.last_cur() {
1358 offset = 0;
1362 } else {
1363 offset -= self.xdot.n_chars() as isize;
1368 if self.xdot.is_cur() {
1369 offset += 1;
1372 }
1373 };
1374
1375 let dot_after_edit = self.dot.with_offset_saturating(offset);
1376 self.dot = self.xdot;
1377 self.handle_action(Action::InsertString { s }, Source::Fsys);
1378 (self.xdot, self.dot) = (self.dot, dot_after_edit);
1379 self.dot.clamp_idx(self.txt.len_chars()); }
1381}
1382
1383fn n_digits(mut n: usize) -> usize {
1384 if n == 0 {
1385 return 1;
1386 }
1387
1388 let mut digits = 0;
1389 while n != 0 {
1390 digits += 1;
1391 n /= 10;
1392 }
1393
1394 digits
1395}
1396
1397#[cfg(test)]
1398pub(crate) mod tests {
1399 use super::*;
1400 use crate::{key::Arrow, syntax::ts::TsState};
1401 use edit::tests::{del_c, del_s, in_c, in_s};
1402 use simple_test_case::test_case;
1403 use std::env;
1404
1405 const LINE_1: &str = "This is a test";
1406 const LINE_2: &str = "involving multiple lines";
1407
1408 fn c(idx: usize) -> Cur {
1409 Cur { idx }
1410 }
1411
1412 fn r(from: usize, to: usize, from_active: bool) -> Range {
1413 Range::from_cursors(c(from), c(to), from_active)
1414 }
1415
1416 pub fn buffer_from_lines(lines: &[&str]) -> Buffer {
1417 let mut b = Buffer::new_unnamed(0, "", Default::default());
1418 let s = lines.join("\n");
1419
1420 for c in s.chars() {
1421 b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1422 }
1423
1424 b
1425 }
1426
1427 fn simple_initial_buffer() -> Buffer {
1428 buffer_from_lines(&[LINE_1, LINE_2])
1429 }
1430
1431 #[test_case(0, 1; "n0")]
1432 #[test_case(5, 1; "n5")]
1433 #[test_case(10, 2; "n10")]
1434 #[test_case(13, 2; "n13")]
1435 #[test_case(731, 3; "n731")]
1436 #[test_case(930, 3; "n930")]
1437 #[test]
1438 fn n_digits_works(n: usize, digits: usize) {
1439 assert_eq!(n_digits(n), digits);
1440 }
1441
1442 #[test]
1443 fn simple_insert_works() {
1444 let b = simple_initial_buffer();
1445 let c = Cur::from_yx(1, LINE_2.len(), &b);
1446 let lines = b.string_lines();
1447
1448 assert_eq!(lines.len(), 2);
1449 assert_eq!(lines[0], LINE_1);
1450 assert_eq!(lines[1], LINE_2);
1451 assert_eq!(b.dot, Dot::Cur { c });
1452 assert_eq!(
1453 b.edit_log.edits,
1454 vec![vec![in_s(0, &format!("{LINE_1}\n{LINE_2}"))]]
1455 );
1456 }
1457
1458 #[test]
1459 fn insert_with_moving_dot_works() {
1460 let mut b = Buffer::new_unnamed(0, "", Default::default());
1461
1462 for c in "hello w".chars() {
1464 b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1465 }
1466
1467 b.handle_action(
1469 Action::DotSet(TextObject::Arr(Arrow::Left), 2),
1470 Source::Keyboard,
1471 );
1472 b.handle_action(Action::InsertChar { c: ',' }, Source::Keyboard);
1473
1474 b.handle_action(Action::DotSet(TextObject::LineEnd, 1), Source::Keyboard);
1476 for c in "orld!".chars() {
1477 b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1478 }
1479
1480 assert_eq!(b.txt.to_string(), "hello, world!");
1482 }
1483
1484 #[test_case(
1485 Action::InsertChar { c: 'x' },
1486 in_c(LINE_1.len() + 1, 'x');
1487 "char"
1488 )]
1489 #[test_case(
1490 Action::InsertString { s: "x".to_string() },
1491 in_s(LINE_1.len() + 1, "x");
1492 "string"
1493 )]
1494 #[test]
1495 fn insert_w_range_dot_works(a: Action, edit: Edit) {
1496 let mut b = simple_initial_buffer();
1497 b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1498
1499 let outcome = b.handle_action(a, Source::Keyboard);
1500 assert_eq!(outcome, None);
1501
1502 let lines = b.string_lines();
1503 assert_eq!(lines.len(), 2);
1504
1505 let c = Cur::from_yx(1, 1, &b);
1506 assert_eq!(b.dot, Dot::Cur { c });
1507
1508 assert_eq!(lines[0], LINE_1);
1509 assert_eq!(lines[1], "x");
1510 assert_eq!(
1511 b.edit_log.edits,
1512 vec![vec![
1513 in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1514 del_s(LINE_1.len() + 1, LINE_2),
1515 edit,
1516 ]]
1517 );
1518 }
1519
1520 #[test]
1521 fn move_forward_at_end_of_buffer_is_fine() {
1522 let mut b = Buffer::new_unnamed(0, "", Default::default());
1523 b.handle_raw_input(Input::Arrow(Arrow::Right));
1524
1525 let c = Cur { idx: 0 };
1526 assert_eq!(b.dot, Dot::Cur { c });
1527 }
1528
1529 #[test]
1530 fn delete_in_empty_buffer_is_fine() {
1531 let mut b = Buffer::new_unnamed(0, "", Default::default());
1532 b.handle_action(Action::Delete, Source::Keyboard);
1533 let c = Cur { idx: 0 };
1534 let lines = b.string_lines();
1535
1536 assert_eq!(b.dot, Dot::Cur { c });
1537 assert_eq!(lines.len(), 1);
1538 assert_eq!(lines[0], "");
1539 assert!(b.edit_log.edits.is_empty());
1540 }
1541
1542 #[test]
1543 fn simple_delete_works() {
1544 let mut b = simple_initial_buffer();
1545 b.handle_action(
1546 Action::DotSet(TextObject::Arr(Arrow::Left), 1),
1547 Source::Keyboard,
1548 );
1549 b.handle_action(Action::Delete, Source::Keyboard);
1550
1551 let c = Cur::from_yx(1, LINE_2.len() - 1, &b);
1552 let lines = b.string_lines();
1553
1554 assert_eq!(b.dot, Dot::Cur { c });
1555 assert_eq!(lines.len(), 2);
1556 assert_eq!(lines[0], LINE_1);
1557 assert_eq!(lines[1], "involving multiple line");
1558 assert_eq!(
1559 b.edit_log.edits,
1560 vec![vec![
1561 in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1562 del_c(LINE_1.len() + 24, 's')
1563 ]]
1564 );
1565 }
1566
1567 #[test]
1568 fn delete_range_works() {
1569 let mut b = simple_initial_buffer();
1570 b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1571 b.handle_action(Action::Delete, Source::Keyboard);
1572
1573 let c = Cur::from_yx(1, 0, &b);
1574 let lines = b.string_lines();
1575
1576 assert_eq!(b.dot, Dot::Cur { c });
1577 assert_eq!(lines.len(), 2);
1578 assert_eq!(lines[0], LINE_1);
1579 assert_eq!(lines[1], "");
1580 assert_eq!(
1581 b.edit_log.edits,
1582 vec![vec![
1583 in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1584 del_s(LINE_1.len() + 1, "involving multiple lines")
1585 ]]
1586 );
1587 }
1588
1589 #[test]
1590 fn delete_range_when_start_equals_end_works() {
1591 let mut b = Buffer::new_unnamed(0, "foo", Default::default());
1592 let (cur, deleted) =
1593 b.delete_range(Range::from_cursors(Cur::new(0), Cur::new(0), false), None);
1594
1595 assert_eq!(b.str_contents(), "oo");
1596 assert_eq!(cur, Cur::new(0));
1597 assert_eq!(deleted, None);
1598 }
1599
1600 #[test]
1601 fn delete_undo_works() {
1602 let mut b = simple_initial_buffer();
1603 let original_lines = b.string_lines();
1604 b.new_edit_log_transaction();
1605
1606 b.handle_action(
1607 Action::DotExtendBackward(TextObject::Word, 1),
1608 Source::Keyboard,
1609 );
1610 b.handle_action(Action::Delete, Source::Keyboard);
1611
1612 b.set_dot(TextObject::BufferStart, 1);
1613 b.handle_action(
1614 Action::DotExtendForward(TextObject::Word, 1),
1615 Source::Keyboard,
1616 );
1617 b.handle_action(Action::Delete, Source::Keyboard);
1618
1619 b.handle_action(Action::Undo, Source::Keyboard);
1620
1621 let lines = b.string_lines();
1622
1623 assert_eq!(lines, original_lines);
1624 }
1625
1626 #[test]
1627 fn undo_string_insert_works() {
1628 let initial_content = "foo foo foo\n";
1629 let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1630
1631 b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1632 b.handle_action(Action::Undo, Source::Keyboard);
1633
1634 assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1635 }
1636
1637 #[test]
1638 fn undo_string_delete_works() {
1639 let initial_content = "foo foo foo\n";
1640 let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1641
1642 let r = Range::from_cursors(c(0), c(2), true);
1643 b.delete_dot(Dot::Range { r }, None);
1644 b.handle_action(Action::Undo, Source::Keyboard);
1645
1646 assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1647 }
1648
1649 #[test]
1650 fn undo_string_insert_and_delete_works() {
1651 let initial_content = "foo foo foo\n";
1652 let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1653
1654 let r = Range::from_cursors(c(0), c(2), true);
1655 b.delete_dot(Dot::Range { r }, None);
1656 b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1657
1658 assert_eq!(b.string_lines(), vec!["bar foo foo", ""]);
1659
1660 b.handle_action(Action::Undo, Source::Keyboard);
1661 b.handle_action(Action::Undo, Source::Keyboard);
1662
1663 assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1664 }
1665
1666 #[test_case("foo", None; "unknown format")]
1668 #[test_case("someFunc()", None; "camel case function call")]
1669 #[test_case("some_func()", None; "snake case function call")]
1670 #[test_case("not_a_real_file.rs", None; "file that does not exist")]
1671 #[test_case("README.md", Some("README.md"); "file that exists")]
1672 #[test_case("README.md:12,19", Some("README.md:12,19"); "file that exists with addr")]
1673 #[test_case("README.md:12:19", Some("README.md:12:19"); "file that exists with addr containing colon")]
1674 #[test_case("/dev/null", Some("/dev/null"); "file that exists abs path")]
1675 #[test_case("/dev/null:12-+#", Some("/dev/null:12-+#"); "file that exists abs path with addr")]
1676 #[test_case("http://example.com", Some("http://example.com"); "http url")]
1677 #[test_case("http://example.com/some/path", Some("http://example.com/some/path"); "http url with path")]
1678 #[test_case("http://example.com?foo=1", Some("http://example.com?foo=1"); "http url with query string")]
1679 #[test_case("http://example.com?foo=1&bar=2", Some("http://example.com?foo=1&bar=2"); "http url with multi query string")]
1680 #[test]
1681 fn try_expand_known_works(s: &str, expected: Option<&str>) {
1682 let cwd = env::current_dir().unwrap().display().to_string();
1683 for (l, r) in [(" ", " "), ("(", ")"), ("[", "]"), ("<", ">"), ("{", "}")] {
1685 let b = Buffer::new_output(
1686 0,
1687 format!("{cwd}/+output"),
1688 format!("abc_123 {l}{s}{r}\tmore text"),
1689 Default::default(),
1690 );
1691
1692 for i in 0..s.len() {
1694 let dot = b.try_expand_known(9 + i);
1695 let maybe_content = dot.map(|d| d.content(&b));
1696 assert_eq!(
1697 maybe_content.as_deref(),
1698 expected,
1699 "failed at offset={i} with lr=({l:?}, {r:?})"
1700 )
1701 }
1702 }
1703 }
1704
1705 #[test_case("\r", "\n"; "CR")]
1706 #[test_case("\n", "\n"; "LF")]
1707 #[test_case("\r\n", "\n"; "CRLF")]
1708 #[test_case("foo\rbar", "foo\nbar"; "text either side of CR")]
1709 #[test_case("foo\nbar", "foo\nbar"; "text either side of LF")]
1710 #[test_case("foo\r\nbar", "foo\nbar"; "text either side of CRLF")]
1711 #[test_case("foo\rbar\nbaz\r\nquux", "foo\nbar\nbaz\nquux"; "mixed line endings")]
1712 #[test]
1713 fn normalizes_line_endings_insert_string(s: &str, expected: &str) {
1714 let mut b = Buffer::new_virtual(0, "test", "", Default::default());
1715 b.insert_string(Dot::Cur { c: c(0) }, s.to_string(), None);
1716 assert_eq!(b.str_contents(), expected);
1717 }
1718
1719 #[test_case('\r', "\n"; "CR")]
1720 #[test_case('\n', "\n"; "LF")]
1721 #[test_case('a', "a"; "ascii")]
1722 #[test]
1723 fn normalizes_line_endings_insert_char(ch: char, expected: &str) {
1724 let mut b = Buffer::new_virtual(0, "test", "", Default::default());
1725 b.insert_char(Dot::Cur { c: c(0) }, ch, None);
1726 assert_eq!(b.str_contents(), expected);
1727 }
1728
1729 #[test]
1732 fn insert_string_reducing_buffer_len_works_with_ts_state() {
1733 let mut b = Buffer::new_virtual(0, "test", "fn main() {}", Default::default());
1734 b.syntax_state = Some(SyntaxState::ts(
1735 TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), "", &b.txt)
1736 .unwrap(),
1737 ));
1738
1739 b.set_dot(TextObject::BufferStart, 1);
1740 b.extend_dot_forward(TextObject::BufferEnd, 1);
1741
1742 b.handle_action(
1743 Action::InsertString {
1744 s: "bar".to_owned(),
1745 },
1746 Source::Fsys,
1747 );
1748
1749 assert_eq!(b.txt.to_string(), "bar");
1750 assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 3 } });
1751 }
1752
1753 #[test]
1754 fn insert_char_reducing_buffer_len_works_with_ts_state() {
1755 let mut b = Buffer::new_virtual(0, "test", "fn main() {}", Default::default());
1756 b.syntax_state = Some(SyntaxState::ts(
1757 TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), "", &b.txt)
1758 .unwrap(),
1759 ));
1760
1761 b.set_dot(TextObject::BufferStart, 1);
1762 b.extend_dot_forward(TextObject::BufferEnd, 1);
1763
1764 b.handle_action(Action::InsertChar { c: 'a' }, Source::Fsys);
1765
1766 assert_eq!(b.txt.to_string(), "a");
1767 assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 1 } });
1768 }
1769
1770 #[test]
1771 fn match_indent_works() {
1772 let mut b = Buffer::new_virtual(0, "test", " foo", Default::default());
1773 b.set_dot(TextObject::BufferEnd, 1);
1774 b.handle_raw_input(Input::Return);
1775 assert_eq!(b.txt.to_string(), " foo\n ");
1776 }
1777
1778 #[test]
1779 fn set_dot_eob_single_line_buffer() {
1780 let mut b = Buffer::new_virtual(
1781 0,
1782 "test",
1783 "// does it need to be a doc comment? that is a long enough line to",
1784 Default::default(),
1785 );
1786 b.set_dot(TextObject::BufferEnd, 1);
1787 b.handle_raw_input(Input::Return);
1788
1789 assert_eq!(
1790 b.txt.to_string(),
1791 "// does it need to be a doc comment? that is a long enough line to\n"
1792 );
1793 }
1794
1795 #[test_case(
1796 Dot::from(r(18, 23, false)), "this is a minimal foo",
1797 r(5, 16, false).into(), "is a minimal";
1798 "range after"
1799 )]
1800 #[test_case(
1801 Dot::from(r(0, 2, false)), "foos is a minimal buffer",
1802 r(5, 16, false).into(), "is a minimal";
1803 "range before equal"
1804 )]
1805 #[test_case(
1806 Dot::from(r(0, 3, false)), "foo is a minimal buffer",
1807 r(4, 15, false).into(), "is a minimal";
1808 "range before truncate"
1809 )]
1810 #[test_case(
1811 Dot::from(r(0, 1, false)), "foois is a minimal buffer",
1812 r(6, 17, false).into(), "is a minimal";
1813 "range before expand"
1814 )]
1815 #[test_case(
1816 Dot::from(r(0, 6, false)), "foo a minimal buffer",
1817 c(3).into(), " ";
1818 "range before intersect"
1819 )]
1820 #[test_case(
1821 Dot::from(r(14, 19, false)), "this is a minifooffer",
1822 c(13).into(), "i";
1823 "range after intersect"
1824 )]
1825 #[test_case(
1826 Dot::from(r(8, 9, false)), "this is foominimal buffer",
1827 c(11).into(), "m";
1828 "range inside dot"
1829 )]
1830 #[test_case(
1831 Dot::from(r(4, 18, false)), "thisfoouffer",
1832 c(7).into(), "u";
1833 "range containing dot"
1834 )]
1835 #[test_case(
1836 Dot::from(c(4)), "thisfoo is a minimal buffer",
1837 r(8, 19, false).into(), "is a minimal";
1838 "cursor before dot"
1839 )]
1840 #[test_case(
1841 Dot::from(c(18)), "this is a minimal foobuffer",
1842 r(5, 16, false).into(), "is a minimal";
1843 "cursor after dot"
1844 )]
1845 #[test_case(
1846 Dot::from(c(10)), "this is a foominimal buffer",
1847 c(13).into(), "m";
1848 "cursor inside dot"
1849 )]
1850 #[test]
1851 fn insert_xdot_sets_correct_dot(
1852 xdot: Dot,
1853 expected_content: &str,
1854 expected_dot: Dot,
1855 expected_dot_content: &str,
1856 ) {
1857 let mut b = Buffer::new_virtual(0, "test", "this is a minimal buffer", Default::default());
1858 b.xdot = xdot;
1859 b.dot = r(5, 16, false).into();
1860 assert_eq!(b.dot_contents(), "is a minimal");
1861
1862 b.insert_xdot("foo".into());
1863
1864 assert_eq!(b.str_contents(), expected_content);
1865 assert_eq!(b.dot, expected_dot);
1866 assert_eq!(b.dot_contents(), expected_dot_content);
1867 }
1868}