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