mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Allow transcript selection and copying while bottom modals are open (#50564)
## Why The plan confirmation prompt allowed transcript scrolling but blocked selecting and copying the visible plan text. ## What changed Allow mouse selection and copying in the visible transcript while a bottom modal is open, preserving drags across redraws. Keep other keyboard input with the modal and suppress transcript link activation, disclosure toggles, and follow-control clicks during modal mouse handling. ## Testing Extend the plan prompt test to cover selection, `Ctrl+Insert` copying, selection highlighting, and popup bounds. Add coverage confirming that ordinary and Control-clicks do not open links in selection-only mouse handling. GitOrigin-RevId: 9e1fdfcb4bc18215e62a714f173bc8cb372b090a
This commit is contained in:
@@ -26,6 +26,12 @@ pub(crate) enum ViewAction {
|
||||
OpenLink(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum MouseMode {
|
||||
Interactive,
|
||||
Selection,
|
||||
}
|
||||
|
||||
/// Transcript jumps shared with input routing so legacy terminals preserve their Alt modifier.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum JumpTarget {
|
||||
@@ -200,10 +206,31 @@ impl TranscriptView {
|
||||
event: MouseEvent,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
) -> Option<ViewAction> {
|
||||
if let Some(action) = self.handle_follow_control_mouse(event) {
|
||||
self.handle_mouse_with_mode(event, cells, MouseMode::Interactive)
|
||||
}
|
||||
|
||||
/// Handles selection and scrolling without activating transcript content.
|
||||
pub(crate) fn handle_selection_mouse(
|
||||
&mut self,
|
||||
event: MouseEvent,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
) -> Option<ViewAction> {
|
||||
self.handle_mouse_with_mode(event, cells, MouseMode::Selection)
|
||||
}
|
||||
|
||||
fn handle_mouse_with_mode(
|
||||
&mut self,
|
||||
event: MouseEvent,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
mode: MouseMode,
|
||||
) -> Option<ViewAction> {
|
||||
if mode == MouseMode::Interactive
|
||||
&& let Some(action) = self.handle_follow_control_mouse(event)
|
||||
{
|
||||
return Some(action);
|
||||
}
|
||||
if event.kind == MouseEventKind::Down(MouseButton::Left)
|
||||
if mode == MouseMode::Interactive
|
||||
&& event.kind == MouseEventKind::Down(MouseButton::Left)
|
||||
&& event
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
@@ -264,21 +291,28 @@ impl TranscriptView {
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(ViewAction::Copy);
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => return self.pointer_down(event, cells),
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
return self.pointer_down(event, cells, mode);
|
||||
}
|
||||
MouseEventKind::Drag(MouseButton::Left) if dragging => {
|
||||
self.extend_selection(event.column, event.row);
|
||||
}
|
||||
MouseEventKind::Up(MouseButton::Left) if dragging => {
|
||||
// Open only a stationary click. Dragging back to the origin is still selection,
|
||||
// and wheel scrolling clears the pointer even when it cannot move the viewport.
|
||||
let link = self.selection.as_mut().and_then(|selection| {
|
||||
(inside
|
||||
&& !selection.moved
|
||||
&& selection.pointer == Some(ScreenPosition::new(event.column, event.row))
|
||||
&& event.modifiers.is_empty())
|
||||
.then(|| selection.pressed_link.take())
|
||||
.flatten()
|
||||
});
|
||||
let link = if mode == MouseMode::Interactive {
|
||||
self.selection.as_mut().and_then(|selection| {
|
||||
(inside
|
||||
&& !selection.moved
|
||||
&& selection.pointer
|
||||
== Some(ScreenPosition::new(event.column, event.row))
|
||||
&& event.modifiers.is_empty())
|
||||
.then(|| selection.pressed_link.take())
|
||||
.flatten()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let link = link.filter(|destination| {
|
||||
self.link_at(event.column, event.row).as_ref() == Some(destination)
|
||||
});
|
||||
@@ -385,17 +419,22 @@ impl TranscriptView {
|
||||
&mut self,
|
||||
event: MouseEvent,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
mode: MouseMode,
|
||||
) -> Option<ViewAction> {
|
||||
if event.modifiers.is_empty() && self.toggle_disclosure_at(cells, event.column, event.row) {
|
||||
if mode == MouseMode::Interactive
|
||||
&& event.modifiers.is_empty()
|
||||
&& self.toggle_disclosure_at(cells, event.column, event.row)
|
||||
{
|
||||
return Some(ViewAction::Changed);
|
||||
}
|
||||
self.disclosure.focused = None;
|
||||
let visible = self
|
||||
.visible
|
||||
.get(usize::from(event.row.checked_sub(self.area.y)?))?;
|
||||
if event
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
if mode == MouseMode::Interactive
|
||||
&& event
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
{
|
||||
return self
|
||||
.link_at(event.column, event.row)
|
||||
@@ -420,7 +459,7 @@ impl TranscriptView {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let link = (clicks == 1 && event.modifiers.is_empty())
|
||||
let link = (mode == MouseMode::Interactive && clicks == 1 && event.modifiers.is_empty())
|
||||
.then(|| self.link_at(event.column, event.row))
|
||||
.flatten();
|
||||
self.begin_selection(cells, event.column, event.row, clicks);
|
||||
|
||||
@@ -306,6 +306,38 @@ fn ordinary_click_opens_bare_and_markdown_links_on_release() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_mouse_does_not_open_links() {
|
||||
for modifiers in [KeyModifiers::NONE, KeyModifiers::CONTROL] {
|
||||
let (mut view, cells) = transcript("[docs](https://example.com/docs)", /*width*/ 60);
|
||||
let down = MouseEvent {
|
||||
modifiers,
|
||||
..mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
/*column*/ 4,
|
||||
/*row*/ 1,
|
||||
)
|
||||
};
|
||||
assert!(matches!(
|
||||
view.handle_selection_mouse(down, &cells),
|
||||
Some(ViewAction::Changed)
|
||||
));
|
||||
let area = view.area;
|
||||
view.render(area, &mut Buffer::empty(area), &cells);
|
||||
assert!(matches!(
|
||||
view.handle_selection_mouse(
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
..down
|
||||
},
|
||||
&cells,
|
||||
),
|
||||
Some(ViewAction::Changed)
|
||||
));
|
||||
assert_eq!(view.selected_text(&cells), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_link_selects_text_and_never_opens_it() {
|
||||
for return_to_origin in [false, true] {
|
||||
|
||||
Reference in New Issue
Block a user