1use 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
48pub enum EditorMode {
50 Terminal,
52 Headless,
54 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#[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 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 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 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 #[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 #[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 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 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 pub fn run_with_explicit_fsys_path(&mut self, socket_path: PathBuf) {
292 self.run_event_loop(Some(socket_path));
293 }
294
295 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 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 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), 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 #[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 let evt = ed.rx_events.recv().unwrap();
809 ed.handle_event(evt);
810
811 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 sleep(Duration::from_millis(100));
821
822 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}