mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Allow paste in searchable selection menus (#25400)
## Summary I frequently want to be able to paste into the searchable menu -- the most common use-case here is when specifying an upstream for a `/review`, where I copy the upstream from an open terminal.
This commit is contained in:
committed by
GitHub
Unverified
parent
13edafb6ed
commit
12c37a6b5c
@@ -15,6 +15,7 @@ use ratatui::widgets::Widget;
|
||||
use super::selection_popup_common::render_menu_surface;
|
||||
use super::selection_popup_common::wrap_styled_line;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::clipboard_paste::normalize_pasted_search_query;
|
||||
use crate::key_hint::KeyBinding;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::key_hint::is_plain_text_key_event;
|
||||
@@ -1016,6 +1017,18 @@ impl BottomPaneView for ListSelectionView {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_paste(&mut self, pasted: String) -> bool {
|
||||
if !self.is_searchable {
|
||||
return false;
|
||||
}
|
||||
let Some(pasted) = normalize_pasted_search_query(&pasted) else {
|
||||
return false;
|
||||
};
|
||||
self.search_query.push_str(&pasted);
|
||||
self.apply_filter();
|
||||
true
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.completion.is_some()
|
||||
}
|
||||
@@ -1672,6 +1685,61 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_appends_to_search_query_and_filters_items() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = new_view(
|
||||
SelectionViewParams {
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "main -> feature/other".to_string(),
|
||||
search_value: Some("feature/other".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "main -> feature/paste-support".to_string(),
|
||||
search_value: Some("feature/paste-support".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
is_searchable: true,
|
||||
..Default::default()
|
||||
},
|
||||
tx,
|
||||
);
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
||||
|
||||
assert!(view.handle_paste("eature/paste-support\n".to_string()));
|
||||
|
||||
assert_eq!(view.search_query, "feature/paste-support");
|
||||
assert_eq!(view.filtered_indices, vec![1]);
|
||||
assert_eq!(view.selected_actual_idx(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_paste_is_ignored() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = new_view(
|
||||
SelectionViewParams {
|
||||
items: vec![SelectionItem {
|
||||
name: "main".to_string(),
|
||||
search_value: Some("main".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
is_searchable: true,
|
||||
..Default::default()
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
assert!(!view.handle_paste(" \n\t ".to_string()));
|
||||
|
||||
assert_eq!(view.search_query, "");
|
||||
assert_eq!(view.filtered_indices, vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_tabs_changes_visible_items_and_clears_search() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -236,6 +236,12 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag
|
||||
))
|
||||
}
|
||||
|
||||
/// Normalize pasted text for a single-line search query.
|
||||
pub(crate) fn normalize_pasted_search_query(pasted: &str) -> Option<String> {
|
||||
let normalized = pasted.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
(!normalized.is_empty()).then_some(normalized)
|
||||
}
|
||||
|
||||
/// Normalize pasted text that may represent a filesystem path.
|
||||
///
|
||||
/// Supports:
|
||||
@@ -368,6 +374,19 @@ pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pasted_search_query_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn collapses_whitespace() {
|
||||
assert_eq!(
|
||||
normalize_pasted_search_query(" alpha\n\tbeta\r\n gamma "),
|
||||
Some(String::from("alpha beta gamma"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pasted_paths_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::Arc;
|
||||
mod transcript;
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::clipboard_paste::normalize_pasted_search_query;
|
||||
use crate::color::blend;
|
||||
use crate::color::is_light;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
@@ -548,11 +549,6 @@ fn picker_cwd_filter(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_pasted_query(pasted: &str) -> Option<String> {
|
||||
let normalized = pasted.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
(!normalized.is_empty()).then_some(normalized)
|
||||
}
|
||||
|
||||
fn spawn_app_server_page_loader(
|
||||
app_server: AppServerSession,
|
||||
include_non_interactive: bool,
|
||||
@@ -1237,7 +1233,7 @@ impl PickerState {
|
||||
if self.is_transcript_loading() {
|
||||
return;
|
||||
}
|
||||
let Some(pasted) = normalize_pasted_query(&pasted) else {
|
||||
let Some(pasted) = normalize_pasted_search_query(&pasted) else {
|
||||
return;
|
||||
};
|
||||
let mut new_query = self.query.clone();
|
||||
@@ -6228,14 +6224,6 @@ session_picker_view = "dense"
|
||||
assert_eq!(state.query, "resize results");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_pasted_query_collapses_whitespace() {
|
||||
assert_eq!(
|
||||
normalize_pasted_query(" alpha\n\tbeta\r\n gamma "),
|
||||
Some(String::from("alpha beta gamma"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whitespace_only_paste_is_ignored() {
|
||||
let loader = page_only_loader(|_| {});
|
||||
|
||||
Reference in New Issue
Block a user