mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
tui: make Cmd+Shift+V paste text inline without placeholder
This commit is contained in:
@@ -641,6 +641,18 @@ impl ChatComposer {
|
||||
true
|
||||
}
|
||||
|
||||
/// Integrate pasted text as literal text, without creating large-paste placeholders.
|
||||
///
|
||||
/// This powers the Cmd+Shift+V shortcut so users can intentionally paste large blocks while
|
||||
/// keeping the full text visible/editable in the textarea.
|
||||
pub(crate) fn handle_verbatim_paste(&mut self, pasted: String) -> bool {
|
||||
let pasted = pasted.replace("\r\n", "\n").replace('\r', "\n");
|
||||
self.textarea.insert_str(&pasted);
|
||||
self.paste_burst.clear_after_explicit_paste();
|
||||
self.sync_popups();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn handle_paste_image_path(&mut self, pasted: String) -> bool {
|
||||
let Some(path_buf) = normalize_pasted_path(&pasted) else {
|
||||
return false;
|
||||
@@ -5263,6 +5275,37 @@ mod tests {
|
||||
assert!(composer.pending_pastes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_verbatim_paste_large_inserts_text_without_placeholder() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
|
||||
let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10);
|
||||
let needs_redraw = composer.handle_verbatim_paste(large.clone());
|
||||
assert!(needs_redraw);
|
||||
assert_eq!(composer.textarea.text(), large);
|
||||
assert!(composer.pending_pastes.is_empty());
|
||||
|
||||
let (result, _) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match result {
|
||||
InputResult::Submitted { text, .. } => assert_eq!(text, large),
|
||||
_ => panic!("expected Submitted"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Behavior: editing that removes a paste placeholder should also clear the associated
|
||||
/// `pending_pastes` entry so it cannot be submitted accidentally.
|
||||
#[test]
|
||||
|
||||
@@ -427,6 +427,23 @@ impl BottomPane {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_verbatim_paste(&mut self, pasted: String) {
|
||||
if let Some(view) = self.view_stack.last_mut() {
|
||||
let needs_redraw = view.handle_paste(pasted);
|
||||
if view.is_complete() {
|
||||
self.on_active_view_complete();
|
||||
}
|
||||
if needs_redraw {
|
||||
self.request_redraw();
|
||||
}
|
||||
} else {
|
||||
let needs_redraw = self.composer.handle_verbatim_paste(pasted);
|
||||
if needs_redraw {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert_str(&mut self, text: &str) {
|
||||
self.composer.insert_str(text);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -184,6 +184,7 @@ use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
use crate::clipboard_paste::paste_text;
|
||||
use crate::collaboration_modes;
|
||||
use crate::diff_render::display_path_for;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
@@ -3090,6 +3091,27 @@ impl ChatWidget {
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} if modifiers.contains(KeyModifiers::SUPER)
|
||||
&& modifiers.contains(KeyModifiers::SHIFT)
|
||||
&& !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
&& c.eq_ignore_ascii_case(&'v') =>
|
||||
{
|
||||
match paste_text() {
|
||||
Ok(text) => self.bottom_pane.handle_verbatim_paste(text),
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to paste text: {err}");
|
||||
self.add_to_history(history_cell::new_error_event(format!(
|
||||
"Failed to paste text: {err}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
other if other.kind == KeyEventKind::Press => {
|
||||
self.bottom_pane.clear_quit_shortcut_hint();
|
||||
self.quit_shortcut_expires_at = None;
|
||||
|
||||
@@ -22,6 +22,23 @@ impl std::fmt::Display for PasteImageError {
|
||||
}
|
||||
impl std::error::Error for PasteImageError {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PasteTextError {
|
||||
ClipboardUnavailable(String),
|
||||
NoText(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PasteTextError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PasteTextError::ClipboardUnavailable(msg) => write!(f, "clipboard unavailable: {msg}"),
|
||||
PasteTextError::NoText(msg) => write!(f, "no text on clipboard: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PasteTextError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodedImageFormat {
|
||||
Png,
|
||||
@@ -149,6 +166,22 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn paste_text() -> Result<String, PasteTextError> {
|
||||
let mut cb = arboard::Clipboard::new()
|
||||
.map_err(|e| PasteTextError::ClipboardUnavailable(e.to_string()))?;
|
||||
cb.get()
|
||||
.text()
|
||||
.map_err(|e| PasteTextError::NoText(e.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn paste_text() -> Result<String, PasteTextError> {
|
||||
Err(PasteTextError::ClipboardUnavailable(
|
||||
"clipboard text paste is unsupported on Android".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt WSL fallback for clipboard image paste.
|
||||
///
|
||||
/// If clipboard is unavailable (common under WSL because arboard cannot access
|
||||
|
||||
Reference in New Issue
Block a user