diff options
author | Cosmin Popescu <cosminadrianpopescu@gmail.com> | 2022-06-06 09:20:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-06 09:20:07 +0200 |
commit | e1fcf3a6dbc7a65a341127fab2fca7b4fb2d081b (patch) | |
tree | ec1061b57347348f081d64c532f7fc12ff21c4dd | |
parent | 58cc8fb2e1a091dca72fdbaf9150b200e130fa73 (diff) |
feat(scroll): edit scrollback with default editor (#1456)
* initial commit for opening the current buffer in an editor
* fix(editor): take hidden panes into consideration when manipulating tiled grid
* when closing an edit buffer, take the geometry of the replaced buffer from the closed buffer
* if the floating panels are displayed, don't add to hidden panels the current buffer
* strategy changing - put the panels inside a suppressed_panels HashMap instead of hidden_panels
* Revert "strategy changing - put the panels inside a suppressed_panels HashMap instead of hidden_panels"
This reverts commit c52a203a20cf4c87c147be8b9c193ed6458c1038.
* remove the floating panes by moving them to the tiled_panes in hidden_panels
* feat(edit): open editor to correct line and don't crash when none is set
* formatting
* feat(edit): use suppressed panes
* style(fmt): rustfmt and logs
* style(fmt): clean up unused code
* test(editor): integration test for suppressing/closing suppressed pane
* test(e2e): editor e2e test
* style(fmt): rustfmt
* feat(edit): update ui and setup
* style(fmt): rustfmt
* feat(config): allow configuring scrollback_editor explicitly
* style(fmt): rustfmt
* chore(repo): build after merging
Co-authored-by: Aram Drevekenin <aram@poor.dev>
36 files changed, 978 insertions, 59 deletions
diff --git a/Cargo.lock b/Cargo.lock index b7edee505..9f734a1e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2727,6 +2727,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" [[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.6", + "serde", +] + +[[package]] name = "value-bag" version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3262,6 +3272,7 @@ dependencies = [ "typetag", "unicode-width", "url", + "uuid", "wasmer", "wasmer-wasi", "zellij-tile", diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md index 8f24bbc5b..877141b9a 100644 --- a/docs/MANPAGE.md +++ b/docs/MANPAGE.md @@ -152,6 +152,7 @@ ACTIONS * __MoveFocus: <Direction\>__ - moves focus in the specified direction (Left, Right, Up, Down). * __DumpScreen: <File\>__ - dumps the screen in the specified file. +* __EditScrollback__ - replaces the current pane with the scrollback buffer. * __ScrollUp__ - scrolls up 1 line in the focused pane. * __ScrollDown__ - scrolls down 1 line in the focused pane. * __PageScrollUp__ - scrolls up 1 page in the focused pane. diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 92438525c..00cf27153 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -38,6 +38,7 @@ pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f +pub const EDIT_SCROLLBACK: [u8; 1] = [101]; // e pub const RESIZE_MODE: [u8; 1] = [14]; // ctrl-n pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j @@ -1804,3 +1805,51 @@ pub fn tmux_mode() { }; assert_snapshot!(last_snapshot); } + +#[test] +#[ignore] +pub fn edit_scrollback() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size); + let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) + { + remote_terminal.send_key(&SCROLL_MODE); + remote_terminal.send_key(&EDIT_SCROLLBACK); + step_is_complete = true; + } + step_is_complete + }, + }); + runner.run_all_steps(); + let last_snapshot = runner.take_snapshot_after(Step { + name: "Wait for editor to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.snapshot_contains(".dump") { + // the .dump is an indication we get on the bottom line of vi when editing a + // file + // the temp file name is randomly generated, so we don't assert the whole snapshot + step_is_complete = true; + } + step_is_complete + }, + }); + if runner.test_timed_out && test_attempts > 0 { + test_attempts -= 1; + continue; + } else { + break last_snapshot; + } + }; + assert!(last_snapshot.contains(".dump")); +} diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 466b18052..564714bb7 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -17,6 +17,7 @@ use std::cell::RefCell; use std::rc::Rc; const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; +const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; const ZELLIJ_DATA_DIR: &str = "/usr/src/zellij/e2e-data"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -64,8 +65,8 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {}\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -78,8 +79,8 @@ fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {} options --mirror-session true\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {} options --mirror-session true\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -92,8 +93,12 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr channel .write_all( format!( - "{} --session {} --data-dir {} options --mirror-session {}\n", - ZELLIJ_EXECUTABLE_LOCATION, session_name, ZELLIJ_DATA_DIR, mirrored + "{} {} --session {} --data-dir {} options --mirror-session {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + session_name, + ZELLIJ_DATA_DIR, + mirrored ) .as_bytes(), ) @@ -103,7 +108,13 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) { channel - .write_all(format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, session_name).as_bytes()) + .write_all( + format!( + "{} {} attach {}\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, session_name + ) + .as_bytes(), + ) .unwrap(); channel.flush().unwrap(); } @@ -113,8 +124,8 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {} options --no-pane-frames\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {} options --no-pane-frames\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -127,8 +138,12 @@ fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) { channel .write_all( format!( - "{} --layout {} --session {} --data-dir {}\n", - ZELLIJ_EXECUTABLE_LOCATION, layout_path, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --layout {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + layout_path, + SESSION_NAME, + ZELLIJ_DATA_DIR ) .as_bytes(), ) diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap index 664e881b4..422a5616c 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap @@ -1,7 +1,7 @@ --- source: src/tests/e2e/cases.rs +assertion_line: 295 expression: last_snapshot - --- Zellij (e2e-test) Tab #1 ┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────── SCROLL: 1/1 ┐ @@ -26,4 +26,4 @@ expression: last_snapshot │ ││line19 00000000000000000000000000000000000000000000000000█│ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SCROLL <o> SESSION <q> QUIT - <↓↑> Scroll / <PgUp/PgDn> Scroll Page / <u/d> Scroll Half Page / <ENTER> Select pane + <↓↑> Scroll / <PgUp/PgDn> Scroll / <u/d> Scroll / <e> Edit / <ENTER> Select pane diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index d1cf9eb25..74767439d 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -28,6 +28,7 @@ typetag = "0.1.7" chrono = "0.4.19" close_fds = "0.3.2" sysinfo = "0.22.5" +uuid = { version = "0.8.2", features = ["serde", "v4"] } [dev-dependencies] insta = "1.6.0" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 81b66801e..0a587d7c6 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -620,6 +620,7 @@ fn init_session( Some(os_input.clone()), ), opts.debug, + config_options.scrollback_editor.clone(), ); move || pty_thread_main(pty, layout) diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 1e43e47fa..b757ad460 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -145,6 +145,7 @@ fn handle_openpty( /// fn handle_terminal( cmd: RunCommand, + failover_cmd: Option<RunCommand>, orig_termios: termios::Termios, quit_cb: Box<dyn Fn(PaneId) + Send>, ) -> (RawFd, RawFd) { @@ -152,9 +153,12 @@ fn handle_terminal( // parent. match openpty(None, Some(&orig_termios)) { Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb), - Err(e) => { - panic!("failed to start pty{:?}", e); - } + Err(e) => match failover_cmd { + Some(failover_cmd) => handle_terminal(failover_cmd, None, orig_termios, quit_cb), + None => { + panic!("failed to start pty{:?}", e); + } + }, } } @@ -174,19 +178,40 @@ pub fn spawn_terminal( terminal_action: TerminalAction, orig_termios: termios::Termios, quit_cb: Box<dyn Fn(PaneId) + Send>, -) -> (RawFd, RawFd) { + default_editor: Option<PathBuf>, +) -> Result<(RawFd, RawFd), &'static str> { + let mut failover_cmd_args = None; let cmd = match terminal_action { - TerminalAction::OpenFile(file_to_open) => { - if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { - panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)"); + TerminalAction::OpenFile(file_to_open, line_number) => { + if default_editor.is_none() + && env::var("EDITOR").is_err() + && env::var("VISUAL").is_err() + { + return Err( + "No Editor found, consider setting a path to one in $EDITOR or $VISUAL", + ); } - let command = - PathBuf::from(env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap())); + let command = default_editor.unwrap_or_else(|| { + PathBuf::from(env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap())) + }); - let args = vec![file_to_open + let mut args = vec![]; + let file_to_open = file_to_open .into_os_string() .into_string() - .expect("Not valid Utf8 Encoding")]; + .expect("Not valid Utf8 Encoding"); + if let Some(line_number) = line_number { + if command.ends_with("vim") + || command.ends_with("nvim") + || command.ends_with("emacs") + || command.ends_with("nano") + || command.ends_with("kak") + { + failover_cmd_args = Some(vec![file_to_open.clone()]); + args.push(format!("+{}", line_number)); + } + } + args.push(file_to_open); RunCommand { command, args, @@ -195,8 +220,15 @@ pub fn spawn_terminal( } TerminalAction::RunCommand(command) => command, }; + let failover_cmd = if let Some(failover_cmd_args) = failover_cmd_args { + let mut cmd = cmd.clone(); + cmd.args = failover_cmd_args; + Some(cmd) + } else { + None + }; - handle_terminal(cmd, orig_termios, quit_cb) + Ok(handle_terminal(cmd, failover_cmd, orig_termios, quit_cb)) } #[derive(Clone)] @@ -245,7 +277,8 @@ pub trait ServerOsApi: Send + Sync { &self, terminal_action: TerminalAction, quit_cb: Box<dyn Fn(PaneId) + Send>, - ) -> (RawFd, RawFd); + default_editor: Option<PathBuf>, + ) -> Result<(RawFd, RawFd), &'static str>; /// Read bytes from the standard output of the virtual terminal referred to by `fd`. fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; /// Creates an `AsyncReader` that can be used to read from `fd` in an async context @@ -284,9 +317,15 @@ impl ServerOsApi for ServerOsInputOutput { &self, terminal_action: TerminalAction, quit_cb: Box<dyn Fn(PaneId) + Send>, - ) -> (RawFd, RawFd) { + default_editor: Option<PathBuf>, + ) -> Result<(RawFd, RawFd), &'static str> { let orig_termios = self.orig_termios.lock().unwrap(); - spawn_terminal(terminal_action, orig_termios.clone(), quit_cb) + spawn_terminal( + terminal_action, + orig_termios.clone(), + quit_cb, + default_editor, + ) } fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { unistd::read(fd, buf) diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index e4925c365..f8bd46bc3 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -101,6 +101,49 @@ impl FloatingPanes { self.panes.insert(pane_id, pane); self.z_indices.push(pane_id); } + pub fn replace_active_pane( + &mut self, + pane: Box<dyn Pane>, + client_id: ClientId, + ) -> Option<Box<dyn Pane>> { + self.active_panes + .get(&client_id) + .copied() + .and_then(|active_pane_id| self.replace_pane(active_pane_id, pane)) + } + pub fn replace_pane( + &mut self, + pane_id: PaneId, + mut with_pane: Box<dyn Pane>, + ) -> Option<Box<dyn Pane>> { + let with_pane_id = with_pane.pid(); + with_pane.set_content_offset(Offset::frame(1)); + let removed_pane = self.panes.remove(&pane_id).map(|removed_pane| { + let removed_pane_id = removed_pane.pid(); + let with_pane_id = with_pane.pid(); + let removed_pane_geom = removed_pane.current_geom(); + with_pane.set_geom(removed_pane_geom); + self.panes.insert(with_pane_id, with_pane); + let z_index = self + .z_indices + .iter() + .position(|pane_id| pane_id == &removed_pane_id) + .unwrap(); + self.z_indices.remove(z_index); + self.z_indices.insert(z_index, with_pane_id); + removed_pane + }); + + // update the desired_pane_positions to relate to the new pane + if let Some(desired_pane_position) = self.desired_pane_positions.remove(&pane_id) { + self.desired_pane_positions + .insert(with_pane_id, desired_pane_position); + } + + // move clients from the previously active pane to the new pane we just inserted + self.move_clients_between_panes(pane_id, with_pane_id); + removed_pane + } pub fn rem |