mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
fix: refresh preview after mouse selection (#1095)
* fix: refresh preview after mouse selection * Address mouse preview review feedback * feat(ci): upload coverage report to pages for easier browsing (#1096) * feat(ci): upload coverage report to pages for easier browsing * fix: remove anchors * fix: release report * fix: coverage percent * docs: update README * chore: only on master * test: add unit tests for the shell & manpage generators (#1098) * test: add unit tests for the shell & manpage generators * chore: propagate key bindings generation errors * tests: improve coverage to 90% (#1099) * tests: improve coverage to 90% * feat: improve coverage * remove most unix-only tests * Update src/skim_tests.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fixes * chore: misc --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: cleanup --------- Co-authored-by: LoricAndre <57358788+LoricAndre@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Loric ANDRE <loric.andre@pm.me>
This commit is contained in:
parent
7e2cdf3c8e
commit
7d4502201f
|
|
@ -633,7 +633,10 @@ impl App {
|
|||
}
|
||||
}
|
||||
Event::Mouse(mouse_event) => {
|
||||
self.handle_mouse(*mouse_event, tui)?;
|
||||
let events = self.handle_mouse(*mouse_event)?;
|
||||
for evt in events {
|
||||
tui.event_tx.try_send(evt)?;
|
||||
}
|
||||
}
|
||||
Event::InvalidInput => {
|
||||
warn!("Received invalid input");
|
||||
|
|
@ -1332,16 +1335,15 @@ impl App {
|
|||
}
|
||||
|
||||
/// Handle mouse events
|
||||
fn handle_mouse<B: Backend>(&mut self, mouse_event: MouseEvent, tui: &mut Tui<B>) -> Result<()>
|
||||
where
|
||||
B::Error: Send + Sync + 'static,
|
||||
{
|
||||
fn handle_mouse(&mut self, mouse_event: MouseEvent) -> Result<Vec<Event>> {
|
||||
let mouse_pos = ratatui::layout::Position {
|
||||
x: mouse_event.column,
|
||||
y: mouse_event.row,
|
||||
};
|
||||
trace!("Got mouse event {mouse_event:?}");
|
||||
|
||||
let old_current = self.item_list.current;
|
||||
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollUp => {
|
||||
// Check if mouse is over preview area
|
||||
|
|
@ -1349,15 +1351,10 @@ impl App {
|
|||
&& preview_area.contains(mouse_pos)
|
||||
{
|
||||
// Scroll preview up
|
||||
for evt in self.handle_action(&Action::PreviewUp(3))? {
|
||||
tui.event_tx.try_send(evt)?;
|
||||
}
|
||||
return Ok(());
|
||||
return self.handle_action(&Action::PreviewUp(3));
|
||||
}
|
||||
// Otherwise scroll item list up
|
||||
for evt in self.handle_action(&Action::Up(1))? {
|
||||
tui.event_tx.try_send(evt)?;
|
||||
}
|
||||
return self.handle_action(&Action::Up(1));
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
// Check if mouse is over preview area
|
||||
|
|
@ -1365,15 +1362,10 @@ impl App {
|
|||
&& preview_area.contains(mouse_pos)
|
||||
{
|
||||
// Scroll preview down
|
||||
for evt in self.handle_action(&Action::PreviewDown(3))? {
|
||||
tui.event_tx.try_send(evt)?;
|
||||
}
|
||||
return Ok(());
|
||||
return self.handle_action(&Action::PreviewDown(3));
|
||||
}
|
||||
// Otherwise scroll item list down
|
||||
for evt in self.handle_action(&Action::Down(1))? {
|
||||
tui.event_tx.try_send(evt)?;
|
||||
}
|
||||
return self.handle_action(&Action::Down(1));
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
if let Some((inner, scrollbar_col)) = self.scrollbar_column()
|
||||
|
|
@ -1409,8 +1401,13 @@ impl App {
|
|||
// Ignore other mouse events for now
|
||||
}
|
||||
}
|
||||
tui.event_tx.try_send(Event::Render)?;
|
||||
Ok(())
|
||||
|
||||
self.needs_render();
|
||||
|
||||
if self.item_list.current != old_current {
|
||||
return Ok(Self::on_selection_changed());
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
fn toggle_spinner(&mut self) {
|
||||
self.show_spinner = !self.show_spinner;
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ fn mouse_scroll_up_and_down_move_selection() {
|
|||
app.handle_event(&mut tui, &Event::Mouse(mouse(MouseEventKind::ScrollUp, 1, 1)))
|
||||
.unwrap();
|
||||
// A Render event is queued after handling a mouse event.
|
||||
assert!(drain_events(&mut tui).iter().any(|e| matches!(e, Event::Render)));
|
||||
assert!(app.needs_render.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1200,6 +1200,46 @@ fn mouse_scroll_over_preview_area_scrolls_preview() {
|
|||
}
|
||||
}
|
||||
|
||||
/// A left mouse-button press at the given cell.
|
||||
fn mouse_down(col: u16, row: u16) -> MouseEvent {
|
||||
mouse(MouseEventKind::Down(MouseButton::Left), col, row)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_selection_change_requests_preview() -> Result<()> {
|
||||
let mut app = app_with_items(&["a", "b", "c", "d", "e", "f"]);
|
||||
let _ = render(&mut app, 40, 6);
|
||||
let inner = app.list_inner_area();
|
||||
|
||||
// The default layout is bottom-to-top, so the current item (0) sits on the
|
||||
// bottom row; item 1 occupies the row just above it.
|
||||
let item_one_row = inner.y + inner.height - 2;
|
||||
// Clicking a different row moves the cursor and asks for a fresh preview.
|
||||
let events = app.handle_mouse(mouse_down(inner.x, item_one_row))?;
|
||||
|
||||
assert_eq!(app.item_list.current, 1);
|
||||
assert!(events.iter().any(|event| matches!(event, Event::RunPreview)));
|
||||
assert!(app.needs_render.load(Ordering::Relaxed));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_selection_same_item_only_requests_render() -> Result<()> {
|
||||
let mut app = app_with_items(&["a", "b", "c", "d", "e", "f"]);
|
||||
let _ = render(&mut app, 40, 6);
|
||||
let inner = app.list_inner_area();
|
||||
|
||||
// Bottom-to-top layout: the current item (0) renders on the bottom row.
|
||||
let current_row = inner.y + inner.height - 1;
|
||||
// Clicking the already-current row only redraws; no preview is requested.
|
||||
let events = app.handle_mouse(mouse_down(inner.x, current_row))?;
|
||||
|
||||
assert_eq!(app.item_list.current, 0);
|
||||
assert!(!events.iter().any(|event| matches!(event, Event::RunPreview)));
|
||||
assert!(app.needs_render.load(Ordering::Relaxed));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_other_event_is_ignored() {
|
||||
let mut app = app_with_items(&["a"]);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::io::Cursor;
|
|||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use ratatui::backend::TestBackend;
|
||||
use skim::prelude::*;
|
||||
use skim::tui::event::Action;
|
||||
|
|
@ -128,6 +128,12 @@ impl TestHarness {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a mouse event and process any resulting events immediately.
|
||||
pub fn mouse(&mut self, mouse: MouseEvent) -> Result<()> {
|
||||
self.send(Event::Mouse(mouse))?;
|
||||
self.handle_remaining_events()
|
||||
}
|
||||
|
||||
/// Wait for any in-flight reader and matcher to complete.
|
||||
///
|
||||
/// If the reader is still running (e.g., after a reload in interactive mode),
|
||||
|
|
@ -322,6 +328,18 @@ impl TestHarness {
|
|||
// Process any queued events first (including RunPreview)
|
||||
self.tick()?;
|
||||
|
||||
let debounce_timeout = std::time::Duration::from_secs(2);
|
||||
let debounce_start = std::time::Instant::now();
|
||||
while self.skim.app().pending_preview_run {
|
||||
if debounce_start.elapsed() > debounce_timeout {
|
||||
return Err(color_eyre::eyre::eyre!("Timeout waiting for debounced preview to run"));
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
self.send(Event::Heartbeat)?;
|
||||
self.tick()?;
|
||||
}
|
||||
|
||||
// If there's no preview task running, nothing to wait for
|
||||
let has_pending = match self.skim.app().preview.thread_handle {
|
||||
Some(ref handle) => !handle.is_finished(),
|
||||
|
|
@ -633,6 +651,7 @@ macro_rules! snap_color {
|
|||
/// @snap; // Take snapshot
|
||||
/// @char 'f'; // Send single character
|
||||
/// @type "foo"; // Type string
|
||||
/// @mouse(|h| mouse_down(h, 1)); // Send mouse event
|
||||
/// @action Down(1); // Send action
|
||||
/// @key Enter; // Send special key
|
||||
/// @exited 0; // Assert command exited with status code 0
|
||||
|
|
@ -856,6 +875,14 @@ macro_rules! insta_test {
|
|||
insta_test!(@expand $h, $base, $cmds, $count; $($rest)*);
|
||||
};
|
||||
|
||||
// @mouse - build and send a mouse event from the current harness state
|
||||
(@expand $h:ident, $base:ident, $cmds:ident, $count:ident; @mouse ( $mouse:expr ) ; $($rest:tt)*) => {
|
||||
$cmds.push(concat!("@mouse(", stringify!($mouse), ")"));
|
||||
let __mouse = ($mouse)(&$h);
|
||||
$h.mouse(__mouse)?;
|
||||
insta_test!(@expand $h, $base, $cmds, $count; $($rest)*);
|
||||
};
|
||||
|
||||
// @key - send a special key (Enter, Escape, Tab, etc.)
|
||||
(@expand $h:ident, $base:ident, $cmds:ident, $count:ident; @key $key:ident ; $($rest:tt)*) => {
|
||||
$cmds.push(concat!("@key ", stringify!($key)));
|
||||
|
|
|
|||
50
tests/mouse.rs
Normal file
50
tests/mouse.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||
use ratatui::layout::Rect;
|
||||
use skim::tui::BorderType;
|
||||
|
||||
fn mouse_down(column: u16, row: u16) -> MouseEvent {
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column,
|
||||
row,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_inner_area(h: &common::insta::TestHarness) -> Rect {
|
||||
let app = h.skim.app();
|
||||
let list_area = app.layout.list_area;
|
||||
|
||||
if !matches!(app.options.border, BorderType::None | BorderType::ForceOff) {
|
||||
return Rect {
|
||||
x: list_area.x + 1,
|
||||
y: list_area.y + 1,
|
||||
width: list_area.width.saturating_sub(2),
|
||||
height: list_area.height.saturating_sub(2),
|
||||
};
|
||||
}
|
||||
|
||||
list_area
|
||||
}
|
||||
|
||||
fn mouse_down_list_row(h: &common::insta::TestHarness, row: u16) -> MouseEvent {
|
||||
let inner = list_inner_area(h);
|
||||
mouse_down(inner.x, inner.y + row)
|
||||
}
|
||||
|
||||
insta_test!(
|
||||
mouse_selection_refreshes_preview,
|
||||
["first", "second", "third"],
|
||||
&["--layout", "reverse", "--preview", "echo preview:{}"],
|
||||
{
|
||||
@snap;
|
||||
@mouse(|h| mouse_down_list_row(h, 1));
|
||||
@snap;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/mouse.rs
|
||||
description: "input: items [\"first\", \"second\", \"third\"]\noptions: --layout reverse --preview echo preview:{}"
|
||||
---
|
||||
"> │preview:first "
|
||||
" 3/3 0/0│ "
|
||||
"> first │ "
|
||||
" second │ "
|
||||
" third │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
cursor: (1, 3)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/mouse.rs
|
||||
description: "input: items [\"first\", \"second\", \"third\"]\noptions: --layout reverse --preview echo preview:{}\nafter:\n @mouse(|h| mouse_down_list_row(h, 1))"
|
||||
---
|
||||
"> │preview:second "
|
||||
" 3/3 1/0│ "
|
||||
" first │ "
|
||||
"> second │ "
|
||||
" third │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
" │ "
|
||||
cursor: (1, 3)
|
||||
Loading…
Reference in a new issue