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