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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Added
- add Shift+Click to extend the current selection from its anchor
- add `--maximized` and `--fullscreen` flags to start the window in that mode
- persist and restore window size, maximized, and fullscreen state per session
- REP (`CSI Ps b`): repeat the last printed character `Ps` times
Expand Down
5 changes: 4 additions & 1 deletion src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,10 @@ impl App {
match state {
ElementState::Pressed => {
if let Some((mx, my)) = self.state.mouse_pos {
if self.is_double_click(mx, my) {
let shift = self.modifiers.state().shift_key();
if shift {
self.extend_mouse_selection(mx, my);
} else if self.is_double_click(mx, my) {
self.select_word_at(mx, my);
} else {
self.start_mouse_selection(mx, my);
Expand Down
49 changes: 49 additions & 0 deletions src/input/mouse_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ use crate::input::InputMode;

use crate::App;

/// Given the current input mode and a clicked cell, produce the Visual mode that
/// extends an existing anchored selection to the click point: the anchor
/// (`start_*`) is preserved and the cursor (`cur_*`) jumps to the clicked cell.
/// Returns `None` when there is no Visual selection to extend, in which case the
/// caller should start a fresh selection instead.
pub(crate) fn extend_visual_mode(mode: &InputMode, col: usize, row: usize) -> Option<InputMode> {
if let InputMode::Visual {
start_col,
start_row,
..
} = *mode
{
Some(InputMode::Visual {
start_col,
start_row,
cur_col: col,
cur_row: row,
anchored: true,
})
} else {
None
}
}

pub(super) fn open_url(url: &str) {
#[cfg(target_os = "linux")]
{
Expand Down Expand Up @@ -102,6 +126,27 @@ impl App {
}
}

/// Shift+left-press: if there is an existing anchored Visual selection, keep
/// its anchor (`start_*`) and move the cursor (`cur_*`) to the clicked cell so
/// the selection grows from the original anchor to the click point. A following
/// drag extends further and release copies. With no prior Visual selection this
/// behaves like a normal fresh selection from the click.
pub(crate) fn extend_mouse_selection(&mut self, px: f64, py: f64) {
let Some(new_mode) = self
.pane_at_pixel(px, py)
.and_then(|pane_id| self.pixel_to_cell(pane_id, px, py))
.and_then(|(col, row)| extend_visual_mode(self.state.mode(), col, row))
else {
self.start_mouse_selection(px, py);
return;
};
self.state.tab_mut().mode = new_mode;
self.state.mouse_selecting = true;
if let Some(w) = &self.window {
w.request_redraw();
}
}

/// Select the word under the pixel (double-click): leave it highlighted in
/// anchored Visual mode so the user sees what was picked, and copy it to the
/// clipboard. A blank cell selects nothing. `Esc`/click returns to Insert.
Expand Down Expand Up @@ -224,3 +269,7 @@ impl App {
}
}
}

#[cfg(test)]
#[path = "mouse_ops_test.rs"]
mod tests;
58 changes: 58 additions & 0 deletions src/input/mouse_ops_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::extend_visual_mode;
use crate::input::InputMode;

// ── Shift+Click extend-selection decision ────────────────────────────────────

#[test]
fn extend_from_anchored_visual_keeps_anchor_moves_cursor() {
let current = InputMode::Visual {
start_col: 3,
start_row: 1,
cur_col: 5,
cur_row: 2,
anchored: true,
};
let extended = extend_visual_mode(&current, 10, 7).expect("extends an existing selection");
assert_eq!(
extended,
InputMode::Visual {
start_col: 3,
start_row: 1,
cur_col: 10,
cur_row: 7,
anchored: true,
},
"anchor (start_*) is preserved and cursor (cur_*) jumps to the clicked cell"
);
}

#[test]
fn extend_from_unanchored_visual_still_grows_from_anchor() {
// Even a not-yet-anchored Visual selection carries a start; shift-click
// anchors it and extends the cursor to the click point.
let current = InputMode::Visual {
start_col: 0,
start_row: 0,
cur_col: 4,
cur_row: 0,
anchored: false,
};
let extended = extend_visual_mode(&current, 8, 3).expect("extends the visual selection");
assert_eq!(
extended,
InputMode::Visual {
start_col: 0,
start_row: 0,
cur_col: 8,
cur_row: 3,
anchored: true,
}
);
}

#[test]
fn extend_with_no_selection_returns_none_for_fresh_start() {
// No prior Visual selection → caller should fall back to a fresh selection.
assert!(extend_visual_mode(&InputMode::Insert, 2, 2).is_none());
assert!(extend_visual_mode(&InputMode::Normal, 2, 2).is_none());
}