Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions app/src/terminal/alt_screen/alt_screen_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,35 @@ impl AltScreenElement {
SelectionType::from_click_count(click_count)
};

if should_intercept_mouse(&self.model.lock(), mouse_state.modifiers().shift, app) {
ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::Begin {
point,
side,
selection_type,
position: local_position,
}));
let (should_intercept, has_selection) = {
let model = self.model.lock();
(
should_intercept_mouse(&model, mouse_state.modifiers().shift, app),
model.alt_screen().selection().is_some(),
)
};

if should_intercept {
let should_extend_selection = mouse_state.modifiers().shift
&& selection_type == SelectionType::Simple
&& self.highlighted_url.is_none()
&& self.hovered_secret.is_none()
&& has_selection;

if should_extend_selection {
ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::Extend {
point,
side,
position: local_position,
}));
} else {
ctx.dispatch_typed_action(TerminalAction::AltSelect(SelectAction::Begin {
point,
side,
selection_type,
position: local_position,
}));
}
} else {
ctx.dispatch_typed_action(TerminalAction::MaybeClearAltSelect);
ctx.dispatch_typed_action(TerminalAction::AltMouseAction(mouse_state.set_point(point)));
Expand Down
87 changes: 58 additions & 29 deletions app/src/terminal/block_list_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ pub struct BlockListElement {
scroll_position: ScrollPosition,
is_terminal_focused: bool,
is_terminal_selecting: bool,
is_extending_block_text_selection: bool,
/// This map contains the IDs of sessions that were subshells as keys. Their corresponding
/// values are the command that spawned the subshell, which is needed to paint the "flag"
subshell_sessions: HashMap<SessionId, SubshellSource>,
Expand Down Expand Up @@ -911,6 +912,8 @@ impl BlockListElement {
scroll_position: terminal_view_render_context.scroll_position,
is_terminal_focused: terminal_view_render_context.is_terminal_focused,
is_terminal_selecting: terminal_view_render_context.is_terminal_selecting,
is_extending_block_text_selection: terminal_view_render_context
.is_extending_block_text_selection,
subshell_sessions: terminal_view_render_context.spawning_command_for_subshell_sessions,
subshell_flags: HashMap::new(),
size: None,
Expand Down Expand Up @@ -1597,41 +1600,65 @@ impl BlockListElement {
}
}

// If the find bar is open, allow selecting the active block again so users can
// scope find to that block.
let find_bar_open = self.find_model.as_ref(app).is_find_bar_open();
// If there's only a non-simple selection, clear the clicked block to avoid a
// text selection and a block selection being rendered at the same time.
// Note we should only dispatch block selection actions when the mouse is not
// clicking on highlighted links or a rich block or if this mouse_down is unselecting
// text.
if selection_type == SelectionType::Simple
// iTerm-style shift+left-click extends the existing text selection
// by moving the nearest boundary instead of starting a fresh one.
// Only kicks in when there's actually a selection to extend and the
// click isn't on a link or a revealed secret.
let extend_text_selection = modifiers.shift
&& selection_type == SelectionType::Simple
&& self.highlighted_url.is_none()
&& self.hovered_secret.is_none()
&& model.block_list().selection().is_none()
// Clicking on an active long running block should focus that block instead,
// except when the find bar is open, in which case we allow selecting it.
&& (!on_long_running_block || find_bar_open)
{
ctx.dispatch_typed_action(TerminalAction::BlockSelect {
action: BlockSelectAction::MouseDown(Some(block_index)),
should_redetermine_focus,
});
} else {
&& model.block_list().selection().is_some();

if extend_text_selection {
ctx.dispatch_typed_action(TerminalAction::BlockSelect {
action: BlockSelectAction::ClearAllBlocks,
should_redetermine_focus,
});
}
ctx.dispatch_typed_action(TerminalAction::BlockTextSelect(
Comment thread
cola-runner marked this conversation as resolved.
BlockTextSelectAction::Extend {
point,
side,
position,
},
));
} else {
// If the find bar is open, allow selecting the active block again so users can
// scope find to that block.
let find_bar_open = self.find_model.as_ref(app).is_find_bar_open();
// If there's only a non-simple selection, clear the clicked block to avoid a
// text selection and a block selection being rendered at the same time.
// Note we should only dispatch block selection actions when the mouse is not
// clicking on highlighted links or a rich block or if this mouse_down is unselecting
// text.
if selection_type == SelectionType::Simple
&& self.highlighted_url.is_none()
&& self.hovered_secret.is_none()
&& model.block_list().selection().is_none()
// Clicking on an active long running block should focus that block instead,
// except when the find bar is open, in which case we allow selecting it.
&& (!on_long_running_block || find_bar_open)
{
ctx.dispatch_typed_action(TerminalAction::BlockSelect {
action: BlockSelectAction::MouseDown(Some(block_index)),
should_redetermine_focus,
});
} else {
ctx.dispatch_typed_action(TerminalAction::BlockSelect {
action: BlockSelectAction::ClearAllBlocks,
should_redetermine_focus,
});
}

ctx.dispatch_typed_action(TerminalAction::BlockTextSelect(
BlockTextSelectAction::Begin {
point,
side,
selection_type,
position,
},
));
ctx.dispatch_typed_action(TerminalAction::BlockTextSelect(
BlockTextSelectAction::Begin {
point,
side,
selection_type,
position,
},
));
}
}
// While rich content blocks can't be selected like command blocks,
// text selections can still originate in them (i.e. with AI blocks)
Expand Down Expand Up @@ -1972,7 +1999,9 @@ impl BlockListElement {
let side = self
.size_info
.get_mouse_side(position - vec2f(bounds.origin().x(), snackbar_bottom));
if !is_selecting_blocks {
// Shift+Click Extend starts as a text-selection gesture. Keep allowing drag updates
// even though the Shift modifier would otherwise classify the drag as block selection.
if !is_selecting_blocks || self.is_extending_block_text_selection {
if let Some(point) = self.coord_to_point(
SnackbarPoint::underneath_snackbar(position),
ClampingMode::ClampToGrid,
Expand Down
15 changes: 15 additions & 0 deletions app/src/terminal/model/alt_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ impl AltScreen {
self.set_selection(selection);
}

pub fn extend_selection(&mut self, point: Point, side: Side) {
let mut selection = match self.selection.take() {
None => return,
Some(selection) => selection,
};

selection.extend_to_nearest_boundary(point, side);
if selection.is_tail_before_head() {
selection.set_smart_select_side(Direction::Right);
} else {
selection.set_smart_select_side(Direction::Left);
}
self.set_selection(selection);
}

fn set_selection(&mut self, value: Selection) {
self.selection = Some(value);
self.event_proxy
Expand Down
67 changes: 67 additions & 0 deletions app/src/terminal/model/alt_screen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,73 @@ fn test_alt_screen_single_cell_selection() {
);
}

#[test]
fn test_alt_screen_extend_selection_to_nearest_boundary() {
let mut screen = new_alt_screen(default_size());
let semantic_selection = SemanticSelection::mock(false, "");

screen.start_selection(Point::new(2, 1), SelectionType::Simple, Side::Left);
screen.update_selection(Point::new(7, 1), Side::Right);

screen.extend_selection(Point::new(9, 1), Side::Right);
assert_eq!(
screen.selection_range(&semantic_selection),
Some(ExpandedSelectionRange::Regular {
start: Point::new(2, 1),
end: Point::new(9, 1),
reversed: false,
})
);

screen.extend_selection(Point::new(1, 1), Side::Left);
assert_eq!(
screen.selection_range(&semantic_selection),
Some(ExpandedSelectionRange::Regular {
start: Point::new(1, 1),
end: Point::new(9, 1),
reversed: false,
})
);
}

#[test]
fn test_alt_screen_extend_selection_shrinks_inside_selection() {
let mut screen = new_alt_screen(default_size());
let semantic_selection = SemanticSelection::mock(false, "");

screen.start_selection(Point::new(2, 1), SelectionType::Simple, Side::Left);
screen.update_selection(Point::new(8, 1), Side::Right);

screen.extend_selection(Point::new(3, 1), Side::Left);
assert_eq!(
screen.selection_range(&semantic_selection),
Some(ExpandedSelectionRange::Regular {
start: Point::new(3, 1),
end: Point::new(8, 1),
reversed: false,
})
);

screen.extend_selection(Point::new(7, 1), Side::Right);
assert_eq!(
screen.selection_range(&semantic_selection),
Some(ExpandedSelectionRange::Regular {
start: Point::new(3, 1),
end: Point::new(7, 1),
reversed: false,
})
);
}

#[test]
fn test_alt_screen_extend_selection_without_existing_selection_noops() {
let mut screen = new_alt_screen(default_size());

screen.extend_selection(Point::new(3, 1), Side::Right);

assert!(screen.selection().is_none());
}

#[test]
fn test_accumulate_lines_to_scroll() {
let mut screen = new_alt_screen(default_size());
Expand Down
46 changes: 45 additions & 1 deletion app/src/terminal/model/blocks/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ use crate::env_vars::env_var_collection_block::EnvVarCollectionBlock;
use crate::terminal::event::Event as TerminalEvent;
use crate::terminal::model::block::BlockSection;
use crate::terminal::model::index::{Direction, Point, Side};
use crate::terminal::model::selection::{ExpandedSelectionRange, Selection, SelectionDirection};
use crate::terminal::model::selection::{
should_extend_selection_start, ExpandedSelectionRange, Selection, SelectionDirection,
SelectionPoint,
};
use crate::terminal::model::terminal_model::{BlockIndex, WithinBlock};
use crate::terminal::warpify::success_block::WarpifySuccessBlock;
use crate::terminal::GridType;
Expand Down Expand Up @@ -107,6 +110,35 @@ impl BlockListSelection {
}
}

fn selection_point(point: BlockListPoint) -> SelectionPoint {
SelectionPoint {
row: point.row,
col: point.column,
}
}

/// Extend the selection by moving whichever boundary is closest to `point`.
pub fn extend_to_nearest_boundary(&mut self, point: BlockListPoint, side: Side) {
let target = BlockAnchor::new(point, side);
let head_is_selection_start = !Self::points_need_swap(self.head.point, self.tail.point);
let (selection_start, selection_end) = if head_is_selection_start {
(self.head, self.tail)
} else {
(self.tail, self.head)
};

let should_update_selection_start = should_extend_selection_start(
Self::selection_point(point),
Self::selection_point(selection_start.point),
Self::selection_point(selection_end.point),
);

match (should_update_selection_start, head_is_selection_start) {
(true, true) | (false, false) => self.head = target,
(true, false) | (false, true) => self.tail = target,
}
}

/// Given a block list position (offset from the top-left corner of the
/// block list), returns the specific cell (row/column index within a
/// particular grid within a particular block) closest to the given point.
Expand Down Expand Up @@ -443,6 +475,18 @@ impl BlockList {
self.set_selection(selection);
}

/// Used to extend an existing selection to a new BlockListPoint, moving whichever boundary
/// is closest to the point. Must have an existing selection to update.
pub fn extend_selection_to_nearest_boundary(&mut self, point: BlockListPoint, side: Side) {
let Some(mut selection) = self.selection.take() else {
return;
};

selection.extend_to_nearest_boundary(point, side);

self.set_selection(selection);
}

/// Coordinates an update to the tail of the block-text selection. Returns the BlockListPoint
/// of the new tail, if an update was made.
pub fn move_selection_tail(
Expand Down
Loading