1use crate::{
3 buffer::BufferKind,
4 config::Config,
5 config_handle,
6 dot::{Cur, Dot, Range, TextObject},
7 editor::{Editor, MiniBufferSelection},
8 exec::{Addr, Address, Program},
9 fsys::LogEvent,
10 key::{Arrow, Input},
11 mode::Mode,
12 plumb::{MatchOutcome, PlumbingMessage},
13 replace_config,
14 system::System,
15 ui::{StateChange, UserInterface},
16 update_config,
17 util::gen_help_docs,
18};
19use ad_event::Source;
20use std::{
21 env, fs,
22 path::{Path, PathBuf},
23 process::{Command, Stdio},
24 sync::mpsc::Sender,
25};
26use tracing::{debug, error, info, trace, warn};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) enum Actions {
30 Single(Action),
31 Multi(Vec<Action>),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ViewPort {
37 Bottom,
39 Center,
41 Top,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Action {
48 AppendToOutputBuffer { bufid: usize, content: String },
49 ChangeDirectory { path: Option<String> },
50 CommandMode,
51 Delete,
52 DeleteBuffer { force: bool },
53 DeleteColumn { force: bool },
54 DeleteWindow { force: bool },
55 DotCollapseFirst,
56 DotCollapseLast,
57 DotExtendBackward(TextObject, usize),
58 DotExtendForward(TextObject, usize),
59 DotFlip,
60 DotSet(TextObject, usize),
61 DragWindow { direction: Arrow },
62 EditCommand { cmd: String },
63 ExecuteDot,
64 ExecuteString { s: String },
65 Exit { force: bool },
66 ExpandDot,
67 FindFile { new_window: bool },
68 FindRepoFile { new_window: bool },
69 FocusBuffer { id: usize },
70 InsertChar { c: char },
71 InsertString { s: String },
72 JumpListForward,
73 JumpListBack,
74 LoadDot { new_window: bool },
75 MarkClean { bufid: usize },
76 NewEditLogTransaction,
77 NewColumn,
78 NewWindow,
79 NextBuffer,
80 NextColumn,
81 NextWindowInColumn,
82 OpenFile { path: String },
83 OpenFileInNewWindow { path: String },
84 Paste,
85 PreviousBuffer,
86 PreviousColumn,
87 PreviousWindowInColumn,
88 RawInput { i: Input },
89 Redo,
90 ReloadActiveBuffer,
91 ReloadBuffer { id: usize },
92 ReloadConfig,
93 RunMode,
94 SamMode,
95 SaveBuffer { force: bool },
96 SaveBufferAs { path: String, force: bool },
97 SearchInCurrentBuffer,
98 SelectBuffer,
99 SetViewPort(ViewPort),
100 SetMode { m: &'static str },
101 SetStatusMessage { message: String },
102 ShellPipe { cmd: String },
103 ShellReplace { cmd: String },
104 ShellRun { cmd: String },
105 ShellSend { cmd: String },
106 ShowHelp,
107 Undo,
108 UpdateConfig { input: String },
109 ViewLogs,
110 Yank,
111
112 DebugBufferContents,
113 DebugEditLog,
114}
115
116impl<S> Editor<S>
117where
118 S: System,
119{
120 pub(crate) fn change_directory(&mut self, opt_path: Option<String>) {
121 let p = match opt_path {
122 Some(p) => p,
123 None => match env::var("HOME") {
124 Ok(p) => p,
125 Err(e) => {
126 let msg = format!("Unable to determine home directory: {e}");
127 self.set_status_message(&msg);
128 warn!("{msg}");
129 return;
130 }
131 },
132 };
133
134 let new_cwd = match fs::canonicalize(p) {
135 Ok(cwd) => cwd,
136 Err(e) => {
137 self.set_status_message(&format!("Invalid path: {e}"));
138 return;
139 }
140 };
141
142 if let Err(e) = env::set_current_dir(&new_cwd) {
143 let msg = format!("Unable to set working directory: {e}");
144 self.set_status_message(&msg);
145 error!("{msg}");
146 return;
147 };
148
149 debug!(new_cwd=%new_cwd.as_os_str().to_string_lossy(), "setting working directory");
150 self.cwd = new_cwd;
151 self.set_status_message(&self.cwd.display().to_string());
152 }
153
154 pub fn open_file_relative_to_cwd(&mut self, path: &str, new_window: bool) {
157 self.open_file(self.cwd.join(path), new_window);
158 }
159
160 pub fn open_file<P: AsRef<Path>>(&mut self, path: P, new_window: bool) {
162 let path = path.as_ref();
163 debug!(?path, "opening file");
164 let was_empty_scratch = self.layout.is_empty_scratch();
165 let current_id = self.active_buffer_id();
166
167 match self.layout.open_or_focus(path, new_window) {
168 Err(e) => self.set_status_message(&format!("Error opening file: {e}")),
169
170 Ok(Some(new_id)) => {
171 if was_empty_scratch {
172 _ = self.tx_fsys.send(LogEvent::Close(current_id));
173 }
174 _ = self.tx_fsys.send(LogEvent::Open(new_id));
175 _ = self.tx_fsys.send(LogEvent::Focus(new_id));
176 }
177
178 Ok(None) => {
179 match self.layout.active_buffer().state_changed_on_disk() {
180 Ok(true) => {
181 let res = self.minibuffer_prompt("File changed on disk, reload? [y/n]: ");
182 if let Some("y" | "Y" | "yes") = res.as_deref() {
183 let msg = self.layout.active_buffer_mut().reload_from_disk();
184 self.set_status_message(&msg);
185 }
186 }
187 Ok(false) => (),
188 Err(e) => self.set_status_message(&e),
189 }
190 let id = self.active_buffer_id();
191 if id != current_id {
192 _ = self.tx_fsys.send(LogEvent::Focus(id));
193 }
194 }
195 };
196 }
197
198 fn find_file_under_dir(&mut self, d: &Path, new_window: bool) {
199 let cmd = config_handle!().find_command.clone();
200
201 let selection = match cmd.split_once(' ') {
202 Some((cmd, args)) => {
203 self.minibuffer_select_from_command_output("> ", cmd, args.split_whitespace(), d)
204 }
205 None => self.minibuffer_select_from_command_output(
206 "> ",
207 &cmd,
208 std::iter::empty::<&str>(),
209 d,
210 ),
211 };
212
213 if let MiniBufferSelection::Line { line, .. } = selection {
214 self.open_file_relative_to_cwd(&format!("{}/{}", d.display(), line.trim()), new_window);
215 }
216 }
217
218 pub(crate) fn find_file(&mut self, new_window: bool) {
220 let d = self
221 .layout
222 .active_buffer()
223 .dir()
224 .unwrap_or(&self.cwd)
225 .to_owned();
226 self.find_file_under_dir(&d, new_window);
227 }
228
229 pub(crate) fn find_repo_file(&mut self, new_window: bool) {
231 let d = self
232 .layout
233 .active_buffer()
234 .dir()
235 .unwrap_or(&self.cwd)
236 .to_owned();
237 let s = match self.system.run_command_blocking(
238 "git",
239 ["rev-parse", "--show-toplevel"],
240 &d,
241 self.active_buffer_id(),
242 ) {
243 Ok(s) => s,
244 Err(e) => {
245 self.set_status_message(&format!("unable to find git root: {e}"));
246 return;
247 }
248 };
249
250 let root = Path::new(s.trim());
251 self.find_file_under_dir(root, new_window);
252 }
253
254 pub(crate) fn delete_buffer(&mut self, id: usize, force: bool) {
255 match self.layout.buffer_with_id(id) {
256 Some(b) if b.dirty && !force => self.set_status_message("No write since last change"),
257 None => warn!("attempt to close unknown buffer, id={id}"),
258 _ => {
259 _ = self.tx_fsys.send(LogEvent::Close(id));
260 self.clear_input_filter(id);
261 let was_last_buffer = self.layout.close_buffer(id);
262 self.running = !was_last_buffer;
263 }
264 }
265 }
266
267 pub(crate) fn delete_active_window(&mut self, force: bool) {
268 let is_last_window = self.layout.close_active_window();
269 if is_last_window {
270 self.exit(force);
271 }
272 }
273
274 pub(crate) fn delete_active_column(&mut self, force: bool) {
275 let is_last_column = self.layout.close_active_column();
276 if is_last_column {
277 self.exit(force);
278 }
279 }
280
281 pub(crate) fn mark_clean(&mut self, bufid: usize) {
282 if let Some(b) = self.layout.buffer_with_id_mut(bufid) {
283 b.dirty = false;
284 }
285 }
286
287 pub(super) fn save_current_buffer(&mut self, fname: Option<String>, force: bool) {
288 trace!("attempting to save current buffer");
289 let p = match self.get_buffer_save_path(fname) {
290 Some(p) => p,
291 None => return,
292 };
293
294 let msg = self.layout.active_buffer_mut().save_to_disk_at(p, force);
295 self.set_status_message(&msg);
296 let id = self.active_buffer_id();
297 _ = self.tx_fsys.send(LogEvent::Save(id));
298 }
299
300 fn get_buffer_save_path(&mut self, fname: Option<String>) -> Option<PathBuf> {
301 use BufferKind as Bk;
302
303 let desired_path = match (fname, &self.layout.active_buffer().kind) {
304 (None, Bk::File(ref p)) => return Some(p.clone()),
307 (Some(s), Bk::File(_) | Bk::Unnamed) => PathBuf::from(s),
310 (None, Bk::Unnamed) => match self.minibuffer_prompt("Save As: ") {
312 Some(s) => s.into(),
313 None => return None,
314 },
315 (_, Bk::Directory(_) | Bk::Virtual(_) | Bk::Output(_) | Bk::MiniBuffer) => return None,
317 };
318
319 match desired_path.try_exists() {
320 Ok(false) => (),
321 Ok(true) => {
322 if !self.minibuffer_confirm("File already exists") {
323 return None;
324 }
325 }
326 Err(e) => {
327 self.set_status_message(&format!("Unable to check path: {e}"));
328 return None;
329 }
330 }
331
332 self.layout.active_buffer_mut().kind = BufferKind::File(desired_path.clone());
333
334 Some(desired_path)
335 }
336
337 pub(super) fn reload_buffer(&mut self, id: usize) {
338 let msg = match self.layout.buffer_with_id_mut(id) {
339 Some(b) => b.reload_from_disk(),
340 None => return,
342 };
343
344 self.set_status_message(&msg);
345 }
346
347 pub(super) fn reload_config(&mut self) {
348 info!("reloading config");
349 let msg = match Config::try_load() {
350 Ok(config) => {
351 replace_config(config);
352 "config reloaded".to_string()
353 }
354 Err(s) => s,
355 };
356 info!("{msg}");
357
358 self.set_status_message(&msg);
359 self.ui.state_change(StateChange::ConfigUpdated);
360 }
361
362 pub(super) fn reload_active_buffer(&mut self) {
363 let msg = self.layout.active_buffer_mut().reload_from_disk();
364 self.set_status_message(&msg);
365 }
366
367 pub(super) fn update_config(&mut self, input: &str) {
368 info!(%input, "updating config");
369 if let Err(msg) = update_config(input) {
370 self.set_status_message(&msg);
371 }
372 self.ui.state_change(StateChange::ConfigUpdated);
373 }
374
375 pub(super) fn set_mode(&mut self, name: &str) {
376 if let Some((i, _)) = self.modes.iter().enumerate().find(|(_, m)| m.name == name) {
377 self.modes.swap(0, i);
378 self.ui.set_cursor_shape(self.current_cursor_shape());
379 }
380 }
381
382 pub(super) fn exit(&mut self, force: bool) {
383 let dirty_buffers = self.layout.dirty_buffers();
384 if !dirty_buffers.is_empty() && !force {
385 self.set_status_message("No write since last change. Use ':q!' to force exit");
386 self.minibuffer_select_from("No write since last change> ", dirty_buffers);
387 return;
388 }
389
390 self.running = false;
391 }
392
393 pub(super) fn set_clipboard(&mut self, s: String) {
394 trace!("setting clipboard content");
395 match self.system.set_clipboard(&s) {
396 Ok(_) => self.set_status_message("Yanked selection to system clipboard"),
397 Err(e) => self.set_status_message(&format!("Error setting system clipboard: {e}")),
398 }
399 }
400
401 pub(super) fn paste_from_clipboard(&mut self, source: Source) {
402 trace!("pasting from clipboard");
403 match self.system.read_clipboard() {
404 Ok(s) => self.handle_action(Action::InsertString { s }, source),
405 Err(e) => self.set_status_message(&format!("Error reading system clipboard: {e}")),
406 }
407 }
408
409 pub(super) fn search_in_current_buffer(&mut self) {
410 let numbered_lines = self
411 .layout
412 .active_buffer()
413 .string_lines()
414 .into_iter()
415 .enumerate()
416 .map(|(i, line)| format!("{:>4} | {}", i + 1, line))
417 .collect();
418
419 let selection = self.minibuffer_select_from("> ", numbered_lines);
420 if let MiniBufferSelection::Line { cy, .. } = selection {
421 self.layout.active_buffer_mut().dot = Dot::Cur {
422 c: Cur::from_yx(cy, 0, self.layout.active_buffer()),
423 };
424 self.handle_action(Action::DotSet(TextObject::Line, 1), Source::Fsys);
425 self.handle_action(Action::SetViewPort(ViewPort::Center), Source::Fsys);
426 }
427 }
428
429 pub(super) fn fsys_minibuffer(
430 &mut self,
431 prompt: Option<String>,
432 lines: String,
433 tx: Sender<String>,
434 ) {
435 let lines: Vec<String> = lines.split('\n').map(|s| s.to_string()).collect();
436 let prompt: &str = prompt.as_deref().unwrap_or("> ");
437
438 let selection = self.minibuffer_select_from(prompt, lines);
439 let s = match selection {
440 MiniBufferSelection::Line { line, .. } => line,
441 MiniBufferSelection::UserInput { input } => input,
442 MiniBufferSelection::Cancelled => String::new(),
443 };
444
445 _ = tx.send(s);
446 }
447
448 pub(super) fn select_buffer(&mut self) {
449 let selection = self.minibuffer_select_from("> ", self.layout.as_buffer_list());
450 if let MiniBufferSelection::Line { line, .. } = selection {
451 if let Ok(id) = line.split_once(' ').unwrap().0.parse::<usize>() {
453 self.focus_buffer(id);
454 }
455 }
456 }
457
458 pub(super) fn focus_buffer(&mut self, id: usize) {
459 self.layout.focus_id(id);
460 _ = self.tx_fsys.send(LogEvent::Focus(id));
461 }
462
463 pub(super) fn debug_buffer_contents(&mut self) {
464 self.minibuffer_select_from(
465 "<RAW BUFFER> ",
466 self.layout
467 .active_buffer()
468 .string_lines()
469 .into_iter()
470 .map(|l| format!("{:?}", l))
471 .collect(),
472 );
473 }
474
475 pub(super) fn view_logs(&mut self) {
476 self.layout
477 .open_virtual("+logs", self.log_buffer.content(), false)
478 }
479
480 pub(super) fn show_help(&mut self) {
481 self.layout.open_virtual("+help", gen_help_docs(), false)
482 }
483
484 pub(super) fn debug_edit_log(&mut self) {
485 self.minibuffer_select_from("<EDIT LOG> ", self.layout.active_buffer().debug_edit_log());
486 }
487
488 pub(super) fn expand_current_dot(&mut self) {
489 self.layout.active_buffer_mut().expand_cur_dot();
490 }
491
492 pub(super) fn default_load_dot(&mut self, source: Source, load_in_new_window: bool) {
505 let b = self.layout.active_buffer_mut();
506 b.expand_cur_dot();
507 if b.notify_load(source) {
508 return; }
510
511 let s = b.dot.content(b);
512 if s.is_empty() {
513 return;
514 }
515
516 let id = b.id;
517 self.load_string_in_buffer(id, s, load_in_new_window);
518 }
519
520 pub(super) fn load_string_in_buffer(&mut self, id: usize, s: String, load_in_new_window: bool) {
521 let b = match self.layout.buffer_with_id_mut(id) {
522 Some(b) => b,
523 None => return,
524 };
525
526 let wdir = b
527 .dir()
528 .map(|p| p.display().to_string())
529 .or_else(|| Some(self.cwd.display().to_string()));
530
531 let m = PlumbingMessage {
532 src: Some("ad".to_string()),
533 dst: None,
534 wdir,
535 attrs: Default::default(),
536 data: s.clone(),
537 };
538
539 match self.plumbing_rules.plumb(m) {
540 Some(MatchOutcome::Message(m)) => self.handle_plumbing_message(m, load_in_new_window),
541
542 Some(MatchOutcome::Run(cmd)) => {
543 let mut command = Command::new("sh");
544 command
545 .args(["-c", cmd.as_str()])
546 .stdout(Stdio::null())
547 .stderr(Stdio::null());
548 if let Err(e) = command.spawn() {
549 self.set_status_message(&format!("error spawning process: {e}"));
550 };
551 }
552
553 None => self.load_explicit_string(id, s, load_in_new_window),
554 }
555 }
556
557 fn handle_plumbing_message(&mut self, m: PlumbingMessage, load_in_new_window: bool) {
565 let PlumbingMessage { attrs, data, .. } = m;
566 match attrs.get("action") {
567 Some(s) if s == "showdata" => {
568 let filename = attrs
569 .get("filename")
570 .cloned()
571 .unwrap_or_else(|| "+plumbing-message".to_string());
572 self.layout.open_virtual(filename, data, load_in_new_window);
573 }
574
575 _ => {
576 self.open_file(data, load_in_new_window);
577 if let Some(s) = attrs.get("addr") {
578 match Addr::parse(&mut s.chars().peekable()) {
579 Ok(mut addr) => {
580 let b = self.layout.active_buffer_mut();
581 b.dot = b.map_addr(&mut addr);
582 }
583 Err(e) => self.set_status_message(&format!("malformed addr: {e:?}")),
584 }
585 }
586 }
587 }
588 }
589
590 pub(super) fn load_explicit_string(
591 &mut self,
592 bufid: usize,
593 s: String,
594 load_in_new_window: bool,
595 ) {
596 if s.is_empty() {
597 return;
598 }
599
600 let b = match self.layout.buffer_with_id_mut(bufid) {
601 Some(b) => b,
602 None => return,
603 };
604
605 let (maybe_path, maybe_addr) = match s.find(':') {
606 Some(idx) => {
607 let (s, addr) = s.split_at(idx);
608 let (_, addr) = addr.split_at(1);
609 match Addr::parse(&mut addr.chars().peekable()) {
610 Ok(expr) => (s, Some(expr)),
611 Err(_) => (s, None),
612 }
613 }
614 None => (s.as_str(), None),
615 };
616
617 let mut path = Path::new(&maybe_path).to_path_buf();
618 let mut is_file = path.is_absolute() && path.exists();
619
620 if let (false, Some(dir)) = (is_file, b.dir()) {
621 let full_path = dir.join(&path);
622 if full_path.exists() {
623 path = full_path;
624 is_file = true;
625 }
626 }
627
628 if is_file {
629 self.open_file(path, load_in_new_window);
630 if let Some(mut addr) = maybe_addr {
631 let b = self.layout.active_buffer_mut();
632 b.dot = b.map_addr(&mut addr);
633 self.layout.clamp_scroll();
634 self.handle_action(Action::SetViewPort(ViewPort::Center), Source::Fsys);
635 }
636 } else {
637 b.find_forward(&s);
638 self.handle_action(Action::SetViewPort(ViewPort::Center), Source::Fsys);
639 }
640 }
641
642 pub(super) fn default_execute_dot(&mut self, arg: Option<(Range, String)>, source: Source) {
652 let b = self.layout.active_buffer_mut();
653 b.expand_cur_dot();
654 if b.notify_execute(source, arg.clone()) {
655 return; }
657
658 let mut cmd = b.dot.content(b).trim().to_string();
659 if cmd.is_empty() {
660 return;
661 }
662
663 if let Some((_, arg)) = arg {
664 cmd.push(' ');
665 cmd.push_str(&arg);
666 }
667
668 match self.parse_command(&cmd) {
669 Some(actions) => self.handle_actions(actions, source),
670 None => self.run_shell_cmd(&cmd),
671 }
672 }
673
674 pub(super) fn execute_explicit_string(&mut self, bufid: usize, s: String, source: Source) {
675 let current_id = self.active_buffer_id();
676 self.layout.focus_id_silent(bufid);
677
678 match self.parse_command(s.trim()) {
679 Some(actions) => self.handle_actions(actions, source),
680 None => self.run_shell_cmd(s.trim()),
681 }
682
683 self.layout.focus_id_silent(current_id);
684 }
685
686 pub(super) fn execute_command(&mut self, cmd: &str) {
687 debug!(%cmd, "executing command");
688 if let Some(actions) = self.parse_command(cmd.trim_end()) {
689 self.handle_actions(actions, Source::Fsys);
690 }
691 }
692
693 pub(super) fn execute_edit_command(&mut self, cmd: &str) {
694 debug!(%cmd, "executing edit command");
695 let mut prog = match Program::try_parse(cmd) {
696 Ok(prog) => prog,
697 Err(error) => {
698 warn!(?error, "invalid edit command");
699 self.set_status_message(&format!("Invalid edit command: {error:?}"));
700 return;
701 }
702 };
703
704 let mut buf = Vec::new();
705 let fname = self.layout.active_buffer().full_name().to_string();
706 match prog.execute(self.layout.active_buffer_mut(), &fname, &mut buf) {
707 Ok(new_dot) => {
708 self.layout.record_jump_position();
709 self.layout.active_buffer_mut().dot = new_dot;
710 }
711
712 Err(e) => self.set_status_message(&format!("Error running edit command: {e:?}")),
713 }
714
715 if !buf.is_empty() {
716 let s = match String::from_utf8(buf) {
717 Ok(s) => s,
718 Err(e) => {
719 error!(%e, "edit command produced invalid utf8 output");
720 return;
721 }
722 };
723 let id = self.active_buffer_id();
724 self.layout.write_output_for_buffer(id, s, &self.cwd);
725 }
726 }
727
728 pub(super) fn command_mode(&mut self) {
729 self.modes.insert(0, Mode::ephemeral_mode("COMMAND"));
730
731 if let Some(input) = self.minibuffer_prompt(":") {
732 self.execute_command(&input);
733 }
734
735 self.modes.remove(0);
736 }
737
738 pub(super) fn run_mode(&mut self) {
739 self.modes.insert(0, Mode::ephemeral_mode("RUN"));
740
741 if let Some(input) = self.minibuffer_prompt("!") {
742 self.set_status_message(&format!("running {input:?}..."));
743 self.run_shell_cmd(&input);
744 }
745
746 self.modes.remove(0);
747 }
748
749 pub(super) fn sam_mode(&mut self) {
750 self.modes.insert(0, Mode::ephemeral_mode("EDIT"));
751
752 if let Some(input) = self.minibuffer_prompt("Edit> ") {
753 self.execute_edit_command(&input);
754 };
755
756 self.modes.remove(0);
757 }
758
759 pub(super) fn pipe_dot_through_shell_cmd(&mut self, raw_cmd_str: &str) {
760 let (s, d) = {
761 let b = self.layout.active_buffer();
762 (b.dot_contents(), b.dir().unwrap_or(&self.cwd))
763 };
764
765 let id = self.active_buffer_id();
766 let res = self
767 .system
768 .pipe_through_command("sh", ["-c", raw_cmd_str], &s, d, id);
769
770 match res {
771 Ok(s) => self.handle_action(Action::InsertString { s }, Source::Fsys),
772 Err(e) => self.set_status_message(&format!("Error running external command: {e}")),
773 }
774 }
775
776 pub(super) fn replace_dot_with_shell_cmd(&mut self, raw_cmd_str: &str) {
777 let d = self.layout.active_buffer().dir().unwrap_or(&self.cwd);
778 let id = self.active_buffer_id();
779 let res = self
780 .system
781 .run_command_blocking("sh", ["-c", raw_cmd_str], d, id);
782
783 match res {
784 Ok(s) => self.handle_action(Action::InsertString { s }, Source::Fsys),
785 Err(e) => self.set_status_message(&format!("Error running external command: {e}")),
786 }
787 }
788
789 pub(super) fn run_shell_cmd(&mut self, raw_cmd_str: &str) {
790 let d = self.layout.active_buffer().dir().unwrap_or(&self.cwd);
791 let id = self.active_buffer_id();
792 self.system
793 .run_command("sh", ["-c", raw_cmd_str], d, id, self.tx_events.clone());
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use crate::{editor::EditorMode, LogBuffer, PlumbingRules};
801 use simple_test_case::test_case;
802
803 macro_rules! assert_recv {
804 ($brx:expr, $msg:ident, $expected:expr) => {
805 match $brx.try_recv() {
806 Ok(LogEvent::$msg(id)) if id == $expected => (),
807 Ok(msg) => panic!(
808 "expected {}({}) but got {msg:?}",
809 stringify!($msg),
810 $expected
811 ),
812 Err(e) => panic!(
813 "err={e}
814recv {}({})",
815 stringify!($msg),
816 $expected
817 ),
818 }
819 };
820 }
821
822 #[test]
823 fn opening_a_file_sends_the_correct_fsys_messages() {
824 let mut ed = Editor::new(
825 Config::default(),
826 PlumbingRules::default(),
827 EditorMode::Headless,
828 LogBuffer::default(),
829 );
830 let brx = ed.rx_fsys.take().expect("to have fsys channels");
831
832 ed.open_file("foo", false);
833
834 assert_recv!(brx, Close, 0);
836 assert_recv!(brx, Open, 1);
837 assert_recv!(brx, Focus, 1);
838
839 ed.open_file("bar", false);
841 assert_recv!(brx, Open, 2);
842 assert_recv!(brx, Focus, 2);
843
844 ed.open_file("foo", false);
846 assert_recv!(brx, Focus, 1);
847 }
848
849 #[test_case(&[], &[0]; "empty scratch")]
850 #[test_case(&["foo"], &[1]; "one file")]
851 #[test_case(&["foo", "bar"], &[1, 2]; "two files")]
852 #[test]
853 fn ensure_correct_fsys_state_works(files: &[&str], expected_ids: &[usize]) {
854 let mut ed = Editor::new(
855 Config::default(),
856 PlumbingRules::default(),
857 EditorMode::Headless,
858 LogBuffer::default(),
859 );
860 let brx = ed.rx_fsys.take().expect("to have fsys channels");
861
862 for file in files {
863 ed.open_file(file, false);
864 }
865
866 ed.ensure_correct_fsys_state();
867
868 if !files.is_empty() {
869 assert_recv!(brx, Close, 0);
870 }
871
872 for &expected in expected_ids {
873 assert_recv!(brx, Open, expected);
874 assert_recv!(brx, Focus, expected);
875 }
876 }
877}