diff --git a/app/src/terminal/model/block.rs b/app/src/terminal/model/block.rs index e698baaf2a..73209cf5cf 100644 --- a/app/src/terminal/model/block.rs +++ b/app/src/terminal/model/block.rs @@ -2219,6 +2219,14 @@ impl Block { .contents_to_string_force_full_grid_contents(false, None) } + pub fn command_and_output_to_string(&self) -> String { + if self.honor_ps1() { + self.bounds_to_string(self.start_point(), self.end_point()) + } else { + format!("{}\n{}", self.command_to_string(), self.output_to_string()) + } + } + pub fn output_with_secrets_unobfuscated(&self) -> String { self.output_grid() .contents_to_string_with_secrets_unobfuscated(false, None) diff --git a/app/src/terminal/model/block_tests.rs b/app/src/terminal/model/block_tests.rs index ae76dcf0df..6beb97faf7 100644 --- a/app/src/terminal/model/block_tests.rs +++ b/app/src/terminal/model/block_tests.rs @@ -839,6 +839,62 @@ fn test_selection_bounds_all_grids_single_line_lprompt_command() { assert_eq!(all_grids, "lprompt%cmd1\nrprompt\noutput1\noutput2"); } +#[test] +fn test_command_and_output_to_string_includes_ps1_prompt_command_rprompt_and_output() { + let block_index = BlockIndex::zero(); + let mut prompt_and_command_grid = mock_blockgrid("lprompt%cmd1"); + prompt_and_command_grid.finish(); + let mut rprompt_grid = mock_blockgrid("rprompt"); + rprompt_grid.finish(); + let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n"); + output_grid.finish(); + + let mut block = create_test_block_with_grids( + block_index, + prompt_and_command_grid, + rprompt_grid, + output_grid, + true, /* honor_ps1 */ + ); + block.set_raw_prompt_end_point(Some(PromptEndPoint::PromptEnd { + point: Point::new(0, 7), + has_extra_trailing_newline: false, + })); + + assert_eq!( + block.command_and_output_to_string(), + "lprompt%cmd1\nrprompt\noutput1\noutput2" + ); +} + +#[test] +fn test_command_and_output_to_string_excludes_warp_prompt() { + let block_index = BlockIndex::zero(); + let mut prompt_and_command_grid = mock_blockgrid("cmd1"); + prompt_and_command_grid.finish(); + let mut rprompt_grid = mock_blockgrid("rprompt"); + rprompt_grid.finish(); + let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n"); + output_grid.finish(); + + let mut block = create_test_block_with_grids( + block_index, + prompt_and_command_grid, + rprompt_grid, + output_grid, + false, /* honor_ps1 */ + ); + block.set_honor_ps1(false); + let mut prompt_grid = mock_blockgrid("warp_prompt"); + prompt_grid.finish(); + block.set_prompt_grid(prompt_grid); + + assert_eq!( + block.command_and_output_to_string(), + "cmd1\noutput1\noutput2" + ); +} + /// Tests the single line lprompt, with trailing newline, and command case for text selection across grids. #[test] fn test_selection_bounds_all_grids_single_line_lprompt_trailing_newline_command() { diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index b197fb3bf0..2e306fea3c 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -20772,11 +20772,7 @@ impl TerminalView { let block_str = match entity { BlockEntity::Command => block.command_to_string(), BlockEntity::Output => block.output_to_string_force_full_grid_contents(), - BlockEntity::CommandAndOutput => format!( - "{}\n{}", - block.command_to_string(), - block.output_to_string(), - ), + BlockEntity::CommandAndOutput => block.command_and_output_to_string(), BlockEntity::FilteredOutput => block.output_to_string(), }; diff --git a/crates/integration/src/bin/integration.rs b/crates/integration/src/bin/integration.rs index 9679300ae6..bf5bb02565 100644 --- a/crates/integration/src/bin/integration.rs +++ b/crates/integration/src/bin/integration.rs @@ -278,6 +278,8 @@ fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> { register_test!(test_color_overrides_in_prompt_dont_crash); register_test!(test_copy_prompt_from_block_honor_ps1_disabled); register_test!(test_copy_prompt_from_block_honor_ps1_enabled); + register_test!(test_copy_block_command_and_output_honor_ps1_disabled); + register_test!(test_copy_block_command_and_output_honor_ps1_enabled); register_test!(test_copy_prompt_from_input_honor_ps1_disabled); register_test!(test_warp_prompt_unsets_zsh_rprompt); register_test!(test_copy_prompt_from_input_honor_ps1_enabled); diff --git a/crates/integration/src/test.rs b/crates/integration/src/test.rs index f6dc2a61d3..44a7e2b4cf 100644 --- a/crates/integration/src/test.rs +++ b/crates/integration/src/test.rs @@ -5845,6 +5845,101 @@ function prompt {{ ) } +pub fn test_copy_block_command_and_output_honor_ps1_disabled() -> Builder { + let command = "echo WARP_COPY_E2E_OUTPUT"; + new_builder() + .use_tmp_filesystem_for_test_root_directory() + .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) + .with_step(execute_command_for_single_terminal_in_tab( + 0, + command.into(), + ExpectedExitStatus::Success, + (), + )) + .with_step( + new_step_with_default_assertions("Select last block") + .with_keystrokes(&["cmdorctrl-up"]) + .add_assertion(assert_selected_block_index_is_last_renderable()), + ) + .with_steps(open_context_menu_for_selected_block()) + .with_step( + new_step_with_default_assertions("Copy block copies command and output") + .with_click_on_saved_position("Copy") + .add_assertion(assert_clipboard_contains_string(format!( + "{command}\nWARP_COPY_E2E_OUTPUT" + ))), + ) +} + +pub fn test_copy_block_command_and_output_honor_ps1_enabled() -> Builder { + let prompt_text = "this is my custom prompt"; + let command = "echo WARP_PS1_COPY_E2E_OUTPUT"; + new_builder() + // TODO(CORE-2732): Flakey on linux + .set_should_run_test(skip_if_powershell_core_2303) + .with_user_defaults(HashMap::from([( + HonorPS1::storage_key().to_owned(), + true.to_string(), + )])) + .with_setup(move |utils| { + let dir = utils.test_dir(); + write_rc_files_for_test( + &dir, + format!(r#"export PS1="{prompt_text}""#), + [ShellRcType::Bash, ShellRcType::Zsh], + ); + write_rc_files_for_test( + &dir, + format!( + r#" +function fish_prompt + echo -n "{prompt_text}" +end +"# + ), + [ShellRcType::Fish], + ); + write_rc_files_for_test( + &dir, + format!( + r#" +function prompt {{ + "{prompt_text}" +}} +"# + ), + [ShellRcType::PowerShell], + ) + }) + .with_step( + wait_until_bootstrapped_single_pane_for_tab(0).add_assertion(move |app, window_id| { + let input = single_input_view_for_tab(app, window_id, 0); + let input_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).0); + + async_assert_eq!(input_text, prompt_text) + }), + ) + .with_step(execute_command_for_single_terminal_in_tab( + 0, + command.into(), + ExpectedExitStatus::Success, + (), + )) + .with_step( + new_step_with_default_assertions("Select last block") + .with_keystrokes(&["cmdorctrl-up"]) + .add_assertion(assert_selected_block_index_is_last_renderable()), + ) + .with_steps(open_context_menu_for_selected_block()) + .with_step( + new_step_with_default_assertions("Copy block includes PS1 prompt, command, and output") + .with_click_on_saved_position("Copy") + .add_assertion(assert_clipboard_contains_string(format!( + "{prompt_text}{command}\nWARP_PS1_COPY_E2E_OUTPUT" + ))), + ) +} + pub fn test_copy_prompt_from_input_honor_ps1_disabled() -> Builder { new_builder() .use_tmp_filesystem_for_test_root_directory() diff --git a/crates/integration/tests/integration/shell_integration_tests.rs b/crates/integration/tests/integration/shell_integration_tests.rs index b45248c585..8a4f5688d6 100644 --- a/crates/integration/tests/integration/shell_integration_tests.rs +++ b/crates/integration/tests/integration/shell_integration_tests.rs @@ -100,6 +100,8 @@ integration_tests! { // Tests of custom prompt behavior. test_copy_prompt_from_block_honor_ps1_enabled, test_copy_prompt_from_input_honor_ps1_enabled, + test_copy_block_command_and_output_honor_ps1_disabled, + test_copy_block_command_and_output_honor_ps1_enabled, // Tests zsh-specific right-prompt behavior in Warp prompt mode. test_warp_prompt_unsets_zsh_rprompt,