Skip to main content

ad_editor/editor/
actions.rs

1//! Editor actions in response to user input
2use 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/// How the current viewport should be set in relation to dot.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ViewPort {
36    /// Dot at the bottom of the viewport
37    Bottom,
38    /// Dot in the center of the viewport
39    Center,
40    /// Dot at the top of the viewport
41    Top,
42}
43
44/// Supported actions for interacting with the editor state
45#[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    /// Open a file within the editor using a path that is relative to the effective
190    /// directory
191    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    /// Open a file within the editor using a path that is relative to the current working
196    /// directory
197    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    /// Open a new virtual buffer within the editor.
202    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    /// Open a file within the editor
212    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    /// This shells out to the fd command line program
265    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    /// This shells out to the git and fd command line programs
271    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            // File has a known name which is either where we loaded it from or a
396            // path that has been set and verified from the Some(s) case that follows
397            (None, Bk::File(p)) => return Some(p.clone()),
398            // Renaming an existing file or attempting to save a new file created in
399            // the editor: both need verifying
400            (Some(s), Bk::File(_) | Bk::Unnamed) => PathBuf::from(s),
401            // Attempting to save without a name so we prompt for one and verify it
402            (None, Bk::Unnamed) => match self.minibuffer_prompt("Save As: ") {
403                Some(s) => s.into(),
404                None => return None,
405            },
406            // virtual and minibuffer buffers don't support saving and have no save path
407            (_, 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            // Silently ignoring attempts to reload unknown buffers
433            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        // Depending on how the user has provided input for us to work with we may have ended up
524        // with an empty input or entirely whitespace. In both cases we want to avoid presenting
525        // blank minibuffer lines to the user as they just result in visual noise.
526        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    /// Use the minibuffer to select an open buffer and focus it in the active window
544    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            // unwrap is fine here because we know the format of the buf list we are supplying
548            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    /// Default semantics for attempting to load the current dot:
600    ///   - an event filter is in place -> pass to the event filter
601    ///   - a plumbing rule matches the load -> run the plumbing rule
602    ///   - a relative path from the directory of the containing file -> open in ad
603    ///   - an absolute path -> open in ad
604    ///     - if either have a valid addr following a colon then set dot to that addr
605    ///   - search within the current buffer for the next occurrence of dot and select it
606    ///
607    /// Loading and executing of dot is part of what makes ad an unusual editor. The semantics are
608    /// lifted almost directly from acme on plan9 and the curious user is encouraged to read the
609    /// materials available at http://acme.cat-v.org/ to learn more about what is possible with
610    /// such a system.
611    pub(super) fn default_load_dot(&mut self, source: Source, load_in_new_window: bool) {
612        // Grabbing the ID in this way allows us to treat loads in the scratch buffer as being from
613        // the active buffer.
614        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; // input filter in place
619        }
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    /// Handling of plumbing messages that are sent to ad supports several attributes
673    /// which can be set in order to configure the behaviour:
674    ///   - by default the data will be treated as a filepath and opened
675    ///   - if the attr "addr" is set it will be parsed as an Addr and applied
676    ///   - if the attr "action" is set to "showdata" then a new buffer is created to hold the data
677    ///     - if the attr "filename" is set as well then it will be used as the name for the buffer
678    ///     - otherwise the filename will be "+plumbing-message"
679    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    /// Default semantics for attempting to execute the current dot:
753    ///   - an event filter is in place -> pass to the event filter
754    ///   - a valid ad command -> execute the command
755    ///   - attempt to run as a shell command with args
756    ///
757    /// Loading and executing of dot is part of what makes ad an unusual editor. The semantics are
758    /// lifted almost directly from acme on plan9 and the curious user is encouraged to read the
759    /// materials available at http://acme.cat-v.org/ to learn more about what is possible with
760    /// such a system.
761    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; // input filter in place
766        }
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    /// Silently focus `bufid` (no jumplist record) and execute the given string. If executing the
785    /// string doesn't change the focused buffer we then reset back to the buffer that was active.
786    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        // The first open should also close our scratch buffer
987        assert_recv!(brx, Close, 0);
988        assert_recv!(brx, Open, 1);
989        assert_recv!(brx, Focus, 1);
990
991        // Opening a second file should only notify for that file
992        ed.open_file("bar", false);
993        assert_recv!(brx, Open, 2);
994        assert_recv!(brx, Focus, 2);
995
996        // Opening the first file again should just notify for the current file
997        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}