ad_editor/editor/
mod.rs

1//! The main control flow and functionality of the `ad` editor.
2use crate::{
3    LogBuffer,
4    buffer::{ActionOutcome, Buffer, BufferId, WELCOME_SQUIRREL},
5    config::Config,
6    config_handle, die,
7    dot::TextObject,
8    exec::{Addr, Address},
9    fsys::{AdFs, LogEvent, Message, Req},
10    input::Event,
11    key::{Arrow, Input},
12    lsp::{LspManager, LspManagerHandle},
13    mode::{Mode, modes},
14    plumb::PlumbingRules,
15    system::{DefaultSystem, System},
16    term::CurShape,
17    ui::{Layout, SCRATCH_ID, StateChange, Ui, UserInterface},
18};
19use ad_event::Source;
20use std::{
21    env, fmt, panic,
22    path::{Path, PathBuf},
23    sync::{
24        Arc, RwLock,
25        mpsc::{Receiver, Sender, channel},
26    },
27    time::Instant,
28};
29use tracing::{debug, trace};
30
31mod actions;
32mod built_in_commands;
33mod commands;
34mod minibuffer;
35mod mouse;
36
37pub use actions::Action;
38pub use minibuffer::MiniBufferState;
39pub use mouse::Click;
40
41#[cfg(feature = "fuzz")]
42pub use commands::parse_command_fuzz;
43
44pub(crate) use actions::{Actions, ViewPort};
45pub(crate) use built_in_commands::built_in_commands;
46pub(crate) use minibuffer::{MbSelect, MbSelector, MiniBufferSelection};
47
48/// The mode that the [Editor] will run in following a call to [Editor::run].
49pub enum EditorMode {
50    /// Run as a TUI
51    Terminal,
52    /// Run without a user interface
53    Headless,
54    /// Used in scenario tests
55    Boxed(Box<dyn UserInterface>),
56}
57
58impl fmt::Debug for EditorMode {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Terminal => f.debug_struct("EditorMode::Terminal").finish(),
62            Self::Headless => f.debug_struct("EditorMode::Headless").finish(),
63            Self::Boxed(_) => f.debug_struct("EditorMode::Boxed").finish(),
64        }
65    }
66}
67
68/// The main editor state.
69#[derive(Debug)]
70pub struct Editor<S>
71where
72    S: System,
73{
74    config: Arc<RwLock<Config>>,
75    system: S,
76    ui: Ui,
77    cwd: PathBuf,
78    running: bool,
79    modes: Vec<Mode>,
80    pending_keys: Vec<Input>,
81    layout: Layout,
82    lsp_manager: Arc<LspManagerHandle>,
83    tx_events: Sender<Event>,
84    rx_events: Receiver<Event>,
85    tx_fsys: Sender<LogEvent>,
86    rx_fsys: Option<Receiver<LogEvent>>,
87    log_buffer: LogBuffer,
88    plumbing_rules: PlumbingRules,
89    held_click: Option<Click>,
90    last_click_was_left: bool,
91    last_click_time: Instant,
92}
93
94impl Editor<DefaultSystem> {
95    /// Construct a new [Editor] with the provided config.
96    pub fn new(
97        cfg: Config,
98        plumbing_rules: PlumbingRules,
99        mode: EditorMode,
100        log_buffer: LogBuffer,
101    ) -> Self {
102        Self::new_with_system(
103            cfg,
104            plumbing_rules,
105            mode,
106            log_buffer,
107            DefaultSystem::from_env(),
108        )
109    }
110
111    pub fn new_with_initial_files(
112        config_res: Result<Config, String>,
113        plumbing_rules_res: Result<PlumbingRules, String>,
114        mode: EditorMode,
115        log_buffer: LogBuffer,
116        file_paths: &[impl AsRef<Path>],
117    ) -> Self {
118        Self::new_with_system_and_initial_files(
119            config_res,
120            plumbing_rules_res,
121            mode,
122            log_buffer,
123            DefaultSystem::from_env(),
124            file_paths,
125        )
126    }
127}
128
129impl<S> Editor<S>
130where
131    S: System,
132{
133    /// Construct a new [Editor] with the provided config and System.
134    pub fn new_with_system(
135        config: Config,
136        plumbing_rules: PlumbingRules,
137        mode: EditorMode,
138        log_buffer: LogBuffer,
139        system: S,
140    ) -> Self {
141        let cwd = match env::current_dir() {
142            Ok(cwd) => cwd,
143            Err(e) => die!("Unable to determine working directory: {e}"),
144        };
145        let (tx_events, rx_events) = channel();
146        let (tx_fsys, rx_fsys) = channel();
147
148        let show_splash = config.show_splash;
149        let lsp_manager = Arc::new(LspManager::spawn(
150            config.filetypes.clone(),
151            tx_events.clone(),
152            config.lsp_autostart,
153        ));
154
155        let modes = modes(&config.keys);
156        let config = Arc::new(RwLock::new(config));
157
158        let ui = Ui::new(mode, config.clone());
159        let mut layout = Layout::new(100, 100, lsp_manager.clone(), config.clone());
160        if show_splash && layout.is_empty_squirrel() {
161            layout
162                .active_buffer_mut_ignoring_scratch()
163                .txt
164                .insert_str(0, WELCOME_SQUIRREL);
165        }
166
167        Self {
168            config,
169            system,
170            ui,
171            cwd,
172            running: true,
173            modes,
174            pending_keys: Vec::new(),
175            layout,
176            lsp_manager,
177            tx_events,
178            rx_events,
179            tx_fsys,
180            rx_fsys: Some(rx_fsys),
181            log_buffer,
182            plumbing_rules,
183            held_click: None,
184            last_click_was_left: false,
185            last_click_time: Instant::now(),
186        }
187    }
188
189    /// Construct a new [Editor] using the provided config and then open a list of initial
190    /// files based on their file paths relative to the working directory of the editor.
191    ///
192    /// The config and plumbing rules results are used to display parse errors to the user
193    /// in virtual buffers. In the case that Err is passed for either argument, the default
194    /// for that type will be used to start the editor so the parser errors can be shown.
195    pub fn new_with_system_and_initial_files(
196        config_res: Result<Config, String>,
197        plumbing_rules_res: Result<PlumbingRules, String>,
198        mode: EditorMode,
199        log_buffer: LogBuffer,
200        system: S,
201        file_paths: &[impl AsRef<Path>],
202    ) -> Self {
203        let (config, config_err) = match config_res {
204            Ok(config) => (config, None),
205            Err(e) => (Config::default(), Some(e)),
206        };
207        let (plumbing_rules, plumb_err) = match plumbing_rules_res {
208            Ok(rules) => (rules, None),
209            Err(e) => (PlumbingRules::default(), Some(e)),
210        };
211
212        let mut e = Self::new_with_system(config, plumbing_rules, mode, log_buffer, system);
213
214        for path in file_paths.iter() {
215            e.open_file_relative_to_cwd(path, false);
216        }
217
218        if let Some(err) = config_err {
219            e.open_virtual(
220                "+config-error",
221                format!("Unable to load config file:\n{err}"),
222                true,
223            );
224        }
225        if let Some(err) = plumb_err {
226            e.open_virtual(
227                "+plumbing-error",
228                format!("Unable to load plumbing rules:\n{err}"),
229                true,
230            );
231        }
232
233        e
234    }
235
236    /// The id of the currently active buffer
237    #[inline]
238    pub fn active_buffer_id(&self) -> usize {
239        self.layout.active_buffer_ignoring_scratch().id
240    }
241
242    #[inline]
243    pub fn active_buffer_name(&self) -> &str {
244        self.layout.active_buffer_ignoring_scratch().full_name()
245    }
246
247    pub fn buffer_list(&self) -> Vec<String> {
248        self.layout.as_buffer_list()
249    }
250
251    pub fn buffer_content(&self, id: BufferId) -> Option<String> {
252        self.layout.buffer_with_id(id).map(|b| b.str_contents())
253    }
254
255    pub fn buffer_dot(&self, id: BufferId) -> Option<String> {
256        self.layout.buffer_with_id(id).map(|b| b.dot_contents())
257    }
258
259    pub fn layout_ids(&self) -> Vec<Vec<BufferId>> {
260        self.layout.ids()
261    }
262
263    /// The effective directory of the editor at any point is the directory containing the
264    /// file backing the active buffer, or if the active buffer can not define a containing
265    /// directory, self.cwd.
266    #[inline]
267    pub fn effective_directory(&self) -> &Path {
268        self.layout
269            .active_buffer_ignoring_scratch()
270            .dir()
271            .unwrap_or(&self.cwd)
272    }
273
274    /// Update the stored window size, accounting for the status and message bars
275    /// This will panic if the available screen rows are 0 or 1
276    pub(crate) fn update_window_size(&mut self, screen_rows: usize, screen_cols: usize) {
277        trace!("window size updated: rows={screen_rows} cols={screen_cols}");
278        self.layout.update_screen_size(screen_rows - 2, screen_cols);
279    }
280
281    /// Ensure that opening without any files initialises the fsys state correctly
282    fn ensure_correct_fsys_state(&self) {
283        if self.layout.is_empty_squirrel() {
284            _ = self.tx_fsys.send(LogEvent::Open(0));
285            _ = self.tx_fsys.send(LogEvent::Focus(0));
286        }
287    }
288
289    /// Initialise any UI state required for our [EditorMode] and run the main event loop using a
290    /// custom path for our fsys socket.
291    pub fn run_with_explicit_fsys_path(&mut self, socket_path: PathBuf) {
292        self.run_event_loop(Some(socket_path));
293    }
294
295    /// Initialise any UI state required for our [EditorMode] and run the main event loop.
296    pub fn run(&mut self) {
297        self.run_event_loop(None);
298    }
299
300    fn run_event_loop(&mut self, socket_path: Option<PathBuf>) {
301        let (fs_enabled, auto_mount) = {
302            let cfg = config_handle!(self);
303            (cfg.filesystem.enabled, cfg.filesystem.auto_mount)
304        };
305
306        let handle = if fs_enabled {
307            let rx_fsys = self.rx_fsys.take().expect("to have fsys channels");
308            let fs = AdFs::new(self.tx_events.clone(), rx_fsys, auto_mount);
309            let handle = fs.run_threaded(socket_path);
310            self.ensure_correct_fsys_state();
311
312            Some(handle)
313        } else {
314            None
315        };
316
317        let tx = self.tx_events.clone();
318        let (screen_rows, screen_cols) = self.ui.init(tx);
319        self.update_window_size(screen_rows, screen_cols);
320        self.ui.set_cursor_shape(self.current_cursor_shape());
321
322        while self.running {
323            self.refresh_screen_w_minibuffer(None);
324
325            match self.rx_events.recv() {
326                Ok(next_event) => self.handle_event(next_event),
327                _ => break,
328            }
329        }
330
331        self.ui.shutdown();
332
333        if let Some(handle) = handle {
334            handle.remove_socket();
335        }
336    }
337
338    #[inline]
339    pub fn handle_event(&mut self, event: Event) {
340        match event {
341            Event::Action(a) => self.handle_action(a, Source::Fsys),
342            Event::Actions(a) => self.handle_actions(a, Source::Fsys),
343            Event::BracketedPaste(s) => {
344                self.handle_action(Action::InsertString { s }, Source::Fsys)
345            }
346            Event::Input(i) => self.handle_input(i),
347            Event::Message(msg) => self.handle_message(msg),
348            Event::StatusMessage(msg) => self.set_status_message(msg),
349            Event::WinsizeChanged { rows, cols } => self.update_window_size(rows, cols),
350        }
351    }
352
353    pub fn refresh_screen_w_minibuffer(&mut self, mb: Option<MiniBufferState<'_>>) {
354        self.layout.clamp_scroll();
355        self.ui.refresh(
356            &self.modes[0].name,
357            &mut self.layout,
358            self.system.n_running_children(),
359            &self.pending_keys,
360            self.held_click.as_ref(),
361            mb,
362        );
363    }
364
365    /// Update the status line to contain the given message.
366    pub fn set_status_message(&mut self, msg: impl Into<String>) {
367        self.ui
368            .state_change(StateChange::StatusMessage { msg: msg.into() });
369    }
370
371    pub(crate) fn current_cursor_shape(&self) -> CurShape {
372        self.modes[0].cur_shape
373    }
374
375    pub(crate) fn block_for_input(&mut self) -> Vec<Input> {
376        while self.running {
377            match self.rx_events.recv().unwrap() {
378                Event::Input(i) => return vec![i],
379                Event::BracketedPaste(s) => return s.chars().map(Input::Char).collect(),
380                Event::Action(a) => self.handle_action(a, Source::Fsys),
381                Event::Actions(a) => self.handle_actions(a, Source::Fsys),
382                Event::Message(msg) => self.handle_message(msg),
383                Event::StatusMessage(msg) => self.set_status_message(msg),
384                Event::WinsizeChanged { rows, cols } => self.update_window_size(rows, cols),
385            }
386        }
387
388        Vec::new()
389    }
390
391    fn send_buffer_resp(
392        &self,
393        id: usize,
394        tx: Sender<Result<String, String>>,
395        f: fn(&Buffer) -> String,
396    ) {
397        if id == SCRATCH_ID {
398            _ = tx.send(Ok((f)(self.layout.scratch.b.buffer())));
399            return;
400        }
401
402        match self.layout.buffer_with_id(id) {
403            Some(b) => _ = tx.send(Ok((f)(b))),
404            None => {
405                _ = tx.send(Err("unknown buffer".to_string()));
406                _ = self.tx_fsys.send(LogEvent::Close(id));
407            }
408        }
409    }
410
411    fn handle_buffer_mutation<F: FnOnce(&mut Buffer, String)>(
412        &mut self,
413        id: usize,
414        tx: Sender<Result<String, String>>,
415        s: String,
416        f: F,
417    ) {
418        if id == SCRATCH_ID {
419            (f)(self.layout.scratch.b.buffer_mut(), s);
420            _ = tx.send(Ok("handled".to_string()));
421            return;
422        }
423
424        match self.layout.buffer_with_id_mut(id) {
425            Some(b) => {
426                (f)(b, s);
427                _ = tx.send(Ok("handled".to_string()))
428            }
429
430            None => {
431                _ = tx.send(Err("unknown buffer".to_string()));
432                _ = self.tx_fsys.send(LogEvent::Close(id));
433            }
434        }
435    }
436
437    fn handle_message(&mut self, Message { req, tx }: Message) {
438        use Req::*;
439
440        debug!("received fys message: {req:?}");
441        let default_handled = || _ = tx.send(Ok("handled".to_string()));
442
443        match req {
444            ControlMessage { msg } => {
445                self.execute_command(&msg);
446                default_handled();
447            }
448
449            MinibufferSelect { prompt, lines, tx } => {
450                self.fsys_minibuffer(prompt, lines, tx);
451                default_handled();
452            }
453
454            ReadBufferName { id } => {
455                self.send_buffer_resp(id, tx, |b| format!("{}\n", b.full_name()))
456            }
457            ReadBufferAddr { id } => self.send_buffer_resp(id, tx, |b| format!("{}\n", b.addr())),
458            ReadBufferDot { id } => self.send_buffer_resp(id, tx, |b| b.dot_contents()),
459            ReadBufferXAddr { id } => self.send_buffer_resp(id, tx, |b| format!("{}\n", b.xaddr())),
460            ReadBufferXDot { id } => self.send_buffer_resp(id, tx, |b| b.xdot_contents()),
461            ReadBufferBody { id } => self.send_buffer_resp(id, tx, |b| b.str_contents()),
462            ReadBufferFtype { id } => self.send_buffer_resp(id, tx, |b| {
463                format!(
464                    "{}\n",
465                    b.configured_filetype()
466                        .unwrap_or_else(|| "unknown".to_string())
467                )
468            }),
469
470            SetBufferName { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
471                b.set_filename(s.trim());
472            }),
473
474            SetBufferAddr { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
475                if let Ok(addr) = Addr::parse(s.trim_end()) {
476                    b.dot = b.map_addr(&addr);
477                };
478            }),
479            SetBufferDot { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
480                b.handle_action(Action::InsertString { s }, Source::Fsys);
481            }),
482            SetBufferXAddr { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
483                if let Ok(addr) = Addr::parse(s.trim_end()) {
484                    b.xdot = b.map_addr(&addr);
485                };
486            }),
487            SetBufferXDot { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
488                b.insert_xdot(s);
489            }),
490
491            ClearBufferBody { id } => self.handle_buffer_mutation(id, tx, String::new(), |b, _| {
492                b.clear();
493            }),
494
495            AppendBufferBody { id, s } => self.handle_buffer_mutation(id, tx, s, |b, s| {
496                b.append(s, Source::Fsys);
497            }),
498
499            AppendOutput { id, s } => {
500                self.layout.write_output_for_buffer(id, s, &self.cwd);
501                default_handled();
502            }
503
504            AddInputEventFilter { id, filter } => {
505                let resp = if self.layout.try_set_input_filter(id, filter) {
506                    Ok("handled".to_string())
507                } else {
508                    Err("filter already in place".to_string())
509                };
510                _ = tx.send(resp);
511            }
512
513            RemoveInputEventFilter { id } => {
514                self.layout.clear_input_filter(id);
515                default_handled();
516            }
517
518            LoadInBuffer { id, txt } => {
519                self.load_string_in_buffer(id, txt, false);
520                default_handled();
521            }
522
523            ExecuteInBuffer { id, txt } => {
524                self.execute_explicit_string(id, &txt, Source::Fsys);
525                default_handled();
526            }
527        }
528    }
529
530    pub fn handle_input(&mut self, input: Input) {
531        self.pending_keys.push(input);
532        let maybe_actions = self.modes[0].handle_keys(&mut self.pending_keys);
533
534        if let Some(actions) = maybe_actions {
535            self.handle_actions(actions, Source::Keyboard);
536        }
537    }
538
539    fn handle_explicit_inputs(&mut self, inputs: Vec<Input>) {
540        for i in inputs.into_iter() {
541            self.handle_input(i);
542        }
543    }
544
545    fn handle_actions(&mut self, actions: Actions, source: Source) {
546        match actions {
547            Actions::Single(action) => self.handle_action(action, source),
548            Actions::Multi(actions) => {
549                for action in actions.into_iter() {
550                    self.handle_action(action, source);
551                    if !self.running {
552                        break;
553                    };
554                }
555            }
556        }
557    }
558
559    /// Process a single action and update editor state accordingly
560    pub fn handle_action(&mut self, action: Action, source: Source) {
561        use Action::*;
562
563        match action {
564            Noop => (),
565
566            AppendToOutputBuffer { bufid, content } => self
567                .layout
568                .write_output_for_buffer(bufid, content, &self.cwd),
569            BalanceActiveColumn => self.layout.balance_active_column(),
570            BalanceAll => self.layout.balance_all(),
571            BalanceColumns => self.layout.balance_columns(),
572            BalanceWindows => self.layout.balance_windows(),
573            ChangeDirectory { path } => self.change_directory(path),
574            CleanupChild { id } => self.system.cleanup_child(id),
575            ClearScratch => self.layout.scratch.b.clear(),
576            CommandMode => self.command_mode(),
577            DeleteBuffer { force } => self.delete_buffer(self.active_buffer_id(), force),
578            DeleteColumn { force } => self.delete_active_column(force),
579            DeleteWindow { force } => self.delete_active_window(force),
580            DragWindow {
581                direction: Arrow::Up,
582            } => self.layout.drag_up(),
583            DragWindow {
584                direction: Arrow::Down,
585            } => self.layout.drag_down(),
586            DragWindow {
587                direction: Arrow::Left,
588            } => self.layout.drag_left(),
589            DragWindow {
590                direction: Arrow::Right,
591            } => self.layout.drag_right(),
592            EditCommand { cmd } => self.execute_edit_command(&cmd),
593            EnsureFileIsOpen { path } => self.layout.ensure_file_is_open(&path),
594            ExecuteDot => self.default_execute_dot(None, source),
595            ExecuteString { s } => {
596                self.execute_explicit_string(self.active_buffer_id(), &s, source)
597            }
598            Exit { force } => self.exit(force),
599            ExpandDot => self.expand_current_dot(),
600            FindFile { new_window } => self.find_file(new_window),
601            FindRepoFile { new_window } => self.find_repo_file(new_window),
602            FocusBuffer { id } => self.focus_buffer(id, false), // allow focusing another window
603            JumpListForward => self.jump_forward(),
604            JumpListBack => self.jump_backward(),
605            KillRunningChild => self.kill_running_child(),
606            LoadDot { new_window } => self.default_load_dot(source, new_window),
607            LspShowCapabilities => {
608                if let Some((name, txt)) = self
609                    .lsp_manager
610                    .show_server_capabilities(self.layout.active_buffer_ignoring_scratch())
611                {
612                    self.layout.open_virtual(name, txt, true)
613                }
614            }
615            LspShowDiagnostics => {
616                let action = self
617                    .lsp_manager
618                    .show_diagnostics(self.layout.active_buffer_ignoring_scratch());
619                self.handle_action(action, Source::Fsys);
620            }
621            LspStart => {
622                if let Some(msg) = self.lsp_manager.start_client(self.layout.buffers()) {
623                    self.set_status_message(msg);
624                }
625            }
626            LspStop => self
627                .lsp_manager
628                .stop_client(self.layout.active_buffer_ignoring_scratch()),
629            LspCompletion => self
630                .lsp_manager
631                .completion(self.layout.active_buffer_ignoring_scratch()),
632            LspFormat => self
633                .lsp_manager
634                .format(self.layout.active_buffer_ignoring_scratch()),
635            LspGotoDeclaration => self
636                .lsp_manager
637                .goto_declaration(self.layout.active_buffer_ignoring_scratch()),
638            LspGotoDefinition => self
639                .lsp_manager
640                .goto_definition(self.layout.active_buffer_ignoring_scratch()),
641            LspGotoTypeDefinition => self
642                .lsp_manager
643                .goto_type_definition(self.layout.active_buffer_ignoring_scratch()),
644            LspHover => self
645                .lsp_manager
646                .hover(self.layout.active_buffer_ignoring_scratch()),
647            LspReferences => self
648                .lsp_manager
649                .find_references(self.layout.active_buffer_ignoring_scratch()),
650            LspRename => self.lsp_rename(),
651            LspRenamePrepare => self.prepare_lsp_rename(),
652            MarkClean { bufid } => self.mark_clean(bufid),
653            MbSelect(selector) => selector.run(self),
654            NewEditLogTransaction => self.layout.active_buffer_mut().new_edit_log_transaction(),
655            NewColumn => self.layout.new_column(),
656            NewWindow => self.layout.new_window(),
657            NextBuffer => {
658                let id = self.layout.focus_next_buffer();
659                _ = self.tx_fsys.send(LogEvent::Focus(id));
660            }
661            NextColumn => {
662                self.layout.next_column();
663                let id = self.active_buffer_id();
664                _ = self.tx_fsys.send(LogEvent::Focus(id));
665            }
666            NextWindowInColumn => {
667                self.layout.next_window_in_column();
668                let id = self.active_buffer_id();
669                _ = self.tx_fsys.send(LogEvent::Focus(id));
670            }
671            OpenFile { path } => self.open_file_relative_to_effective_directory(&path, false),
672            OpenFileInNewWindow { path } => {
673                self.open_file_relative_to_effective_directory(&path, true)
674            }
675            OpenTransientScratch { name, txt } => self.layout.open_transient_scratch(name, txt),
676            OpenVirtualFile { name, txt } => self.layout.open_virtual(name, txt, true),
677            Paste => self.paste_from_clipboard(source),
678            Plumb { txt, new_window } => self.plumb(txt, new_window),
679            PreviousBuffer => {
680                let id = self.layout.focus_previous_buffer();
681                _ = self.tx_fsys.send(LogEvent::Focus(id));
682            }
683            PreviousColumn => {
684                self.layout.prev_column();
685                let id = self.active_buffer_id();
686                _ = self.tx_fsys.send(LogEvent::Focus(id));
687            }
688            PreviousWindowInColumn => {
689                self.layout.prev_window_in_column();
690                let id = self.active_buffer_id();
691                _ = self.tx_fsys.send(LogEvent::Focus(id));
692            }
693            ReloadActiveBuffer => self.reload_active_buffer(),
694            ReloadBuffer { id } => self.reload_buffer(id),
695            ReloadConfig => self.reload_config(),
696            ResizeActiveColumn { delta } => self.layout.resize_active_column(delta),
697            ResizeActiveWindow { delta } => self.layout.resize_active_window(delta),
698            RunMode => self.run_mode(),
699            SamMode => self.sam_mode(),
700            SaveBuffer { force } => self.save_current_buffer(None, force),
701            SaveBufferAll { force } => self.save_all_buffers(force),
702            SaveBufferAs { path, force } => self.save_current_buffer(Some(path), force),
703            SearchInCurrentBuffer => self.search_in_current_buffer(),
704            SendKeys { ks } => self.handle_explicit_inputs(ks),
705            SelectBuffer => self.select_buffer(),
706            SetMode { m } => self.set_mode(m),
707            SetStatusMessage { message } => self.set_status_message(&message),
708            SetViewPort(vp) => self.layout.set_viewport(vp),
709            ShellPipe { cmd } => self.pipe_dot_through_shell_cmd(&cmd),
710            ShellReplace { cmd } => self.replace_dot_with_shell_cmd(&cmd),
711            ShellRun { cmd } => self.run_shell_cmd(&cmd),
712            ShowHelp => self.show_help(),
713            ToggleScratch => self.layout.toggle_scratch(),
714            TsShowTree => self.show_active_ts_tree(),
715            ViewLogs => self.view_logs(),
716            Yank => self.set_clipboard(self.layout.active_buffer().dot_contents()),
717
718            DebugBufferContents => self.debug_buffer_contents(),
719            DebugEditLog => self.debug_edit_log(),
720
721            RawInput { i } if i == Input::PageUp || i == Input::PageDown => {
722                let arr = if i == Input::PageUp {
723                    Arrow::Up
724                } else {
725                    Arrow::Down
726                };
727
728                self.forward_action_to_active_buffer(
729                    DotSet(TextObject::Arr(arr), self.layout.active_window_rows()),
730                    Source::Keyboard,
731                );
732            }
733            RawInput {
734                i: Input::Mouse(evt),
735            } => self.handle_mouse_event(evt),
736
737            a => self.forward_action_to_active_buffer(a, source),
738        }
739    }
740
741    fn jump_forward(&mut self) {
742        if let Some(id) = self.layout.jump_forward() {
743            _ = self.tx_fsys.send(LogEvent::Focus(id));
744        }
745    }
746
747    fn jump_backward(&mut self) {
748        if let Some(id) = self.layout.jump_backward() {
749            _ = self.tx_fsys.send(LogEvent::Focus(id));
750        }
751    }
752
753    pub(super) fn forward_action_to_active_buffer(&mut self, a: Action, source: Source) {
754        if let Some(o) = self.layout.active_buffer_mut().handle_action(a, source) {
755            match o {
756                ActionOutcome::SetStatusMessage(msg) => self.set_status_message(&msg),
757                ActionOutcome::SetClipboard(s) => self.set_clipboard(s),
758            }
759        }
760    }
761
762    pub(super) fn forward_action_to_active_buffer_ignoring_scratch(
763        &mut self,
764        a: Action,
765        source: Source,
766    ) {
767        if let Some(o) = self
768            .layout
769            .active_buffer_mut_ignoring_scratch()
770            .handle_action(a, source)
771        {
772            match o {
773                ActionOutcome::SetStatusMessage(msg) => self.set_status_message(&msg),
774                ActionOutcome::SetClipboard(s) => self.set_clipboard(s),
775            }
776        }
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783    use crate::system::DefaultSystem;
784    use std::{thread::sleep, time::Duration};
785
786    // We need access to the internals of Editor for this but really this is a test of the
787    // behaviour of the System trait and DefaultSystem.
788    #[test]
789    fn process_control_works() {
790        let mut ed = Editor::new_with_system(
791            Config::default(),
792            PlumbingRules::default(),
793            EditorMode::Headless,
794            LogBuffer::default(),
795            DefaultSystem::without_clipboard_provider(),
796        );
797
798        ed.update_window_size(100, 80);
799        ed.open_file(ed.cwd.join("test"), false);
800        ed.handle_action(
801            Action::ShellRun {
802                cmd: "yes".to_string(),
803            },
804            Source::Keyboard,
805        );
806
807        // Allow the script to write to the output buffer
808        let evt = ed.rx_events.recv().unwrap();
809        ed.handle_event(evt);
810
811        // Should have the test file and now the output buffer
812        assert_eq!(ed.layout.buffers().len(), 2);
813        assert_eq!(ed.system.running_children().len(), 1);
814
815        ed.system.kill_child(0);
816        assert_eq!(ed.system.running_children().len(), 0);
817
818        // hack: avoid race between the child thread pushing lines to the output buffer and the
819        // attempt to drain the event queue below
820        sleep(Duration::from_millis(100));
821
822        // drain any pending writes from the script
823        while let Ok(evt) = ed.rx_events.try_recv() {
824            match evt {
825                Event::Action(Action::AppendToOutputBuffer { .. }) => (),
826                Event::Action(Action::CleanupChild { .. }) => (),
827                _ => panic!("expected AppendToOutputBuffer or CleanupChild but got {evt:?}"),
828            }
829        }
830
831        ed.layout.close_buffer(1);
832        assert_eq!(ed.layout.buffers().len(), 1);
833
834        match ed.rx_events.try_recv() {
835            Err(_) => (),
836            Ok(Event::Action(Action::CleanupChild { .. })) => (),
837            Ok(evt) => panic!("expected no events or CleanupChild, got {evt:?}"),
838        }
839    }
840}