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
@@ -1,5 +1,6 @@
# Changelog

- **Changed** The `vp run` / `vpr` task selector now wraps from the first task to the last with Up, and from the last task to the first with Down, including filtered results ([#715](https://github.com/voidzero-dev/vite-task/pull/715)).
- **Fixed** `vp run` no longer hangs or fails when a task leaves a process running behind it, such as a dev server or a background helper, or when one of a task's processes is killed. The run finishes as soon as the task itself does, and the files the task used are still recorded ([#544](https://github.com/voidzero-dev/vite-task/issues/544), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** A task that reads or writes an unusually large number of files now runs to the end instead of being killed partway through. Vite+ reports the run as not cached, because it could not record every file the task used ([#533](https://github.com/voidzero-dev/vite-task/issues/533), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)).
Expand Down
116 changes: 112 additions & 4 deletions crates/vt_select/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,16 @@ impl<'a> State<'a> {
}

fn move_up(&mut self) {
if self.selected > 0 {
self.selected -= 1;
if self.item_count > 0 {
self.selected =
if self.selected == 0 { self.item_count - 1 } else { self.selected - 1 };
self.ensure_selected_visible();
}
}

fn move_down(&mut self) {
if self.item_count > 0 && self.selected < self.item_count - 1 {
self.selected += 1;
if self.item_count > 0 {
self.selected = (self.selected + 1) % self.item_count;
self.ensure_selected_visible();
}
}
Expand Down Expand Up @@ -598,6 +599,113 @@ mod tests {
.collect()
}

#[test]
fn navigation_wraps_up_from_first_to_last() {
let items = make_items(&[("build", ""), ("lint", ""), ("test", "")]);
let mut state = State::new(&items, None, 2);

state.move_up();

assert_eq!(state.selected_item_index(), Some(2));
assert_eq!(state.visible_display_rows(), 1..3);
}

#[test]
fn navigation_wraps_down_from_last_to_first() {
let items = make_items(&[("build", ""), ("lint", ""), ("test", "")]);
let mut state = State::new(&items, None, 2);

state.move_down();
assert_eq!(state.selected_item_index(), Some(1));
state.move_down();
assert_eq!(state.selected_item_index(), Some(2));
assert_eq!(state.visible_display_rows(), 1..3);
state.move_down();

assert_eq!(state.selected_item_index(), Some(0));
assert_eq!(state.visible_display_rows(), 0..2);
}

#[test]
fn navigation_wraps_within_filtered_results() {
let items = make_items(&[("build-a", ""), ("lint", ""), ("build-b", ""), ("test", "")]);
let mut state = State::new(&items, None, 2);
state.move_up();
state.query.push_str("build");
state.refilter();
assert_eq!(state.item_count, 2);
assert_eq!(state.selected_item_index(), Some(0));
assert_eq!(state.scroll_offset, 0);

state.move_up();
assert_eq!(state.selected_item_index(), Some(2));
state.move_down();
assert_eq!(state.selected_item_index(), Some(0));
assert_eq!(state.query, "build");
}

#[test]
fn navigation_wraps_past_group_headers_and_scrolls_to_the_selected_item() {
let items = make_grouped_items(&[
("lib#build", "build", "", Some("lib")),
("lib#lint", "lint", "", Some("lib")),
("app#build", "build", "", Some("app")),
]);
let mut state = State::new(&items, None, 3);

state.move_up();
assert_eq!(state.selected_item_index(), Some(2));
assert_eq!(state.visible_display_rows(), 2..5);
state.move_down();
assert_eq!(state.selected_item_index(), Some(0));
assert_eq!(state.visible_display_rows(), 0..3);
assert!(matches!(state.display_rows[0], DisplayRow::Header(_)));

state.move_down();
assert_eq!(state.selected_item_index(), Some(1));
state.move_down();
assert_eq!(state.selected_item_index(), Some(2));
state.move_up();
assert_eq!(state.selected_item_index(), Some(1));
}

#[test]
fn navigation_with_no_items_is_a_noop() {
let mut state = State::new(&[], None, 2);

state.move_up();
state.move_down();

assert_eq!(state.selected, 0);
assert_eq!(state.selected_item_index(), None);
assert_eq!(state.visible_display_rows(), 0..0);
}

#[test]
fn navigation_with_no_matching_items_is_a_noop() {
let items = make_items(&[("build", ""), ("lint", "")]);
let mut state = State::new(&items, Some("zzz"), 2);

state.move_up();
state.move_down();

assert_eq!(state.selected, 0);
assert_eq!(state.selected_item_index(), None);
assert_eq!(state.visible_display_rows(), 0..0);
}

#[test]
fn navigation_with_one_matching_item_stays_on_that_item() {
let items = make_items(&[("build", ""), ("lint", "")]);
let mut state = State::new(&items, Some("lint"), 2);

state.move_up();
assert_eq!(state.selected_item_index(), Some(1));
state.move_down();
assert_eq!(state.selected_item_index(), Some(1));
assert_eq!(state.visible_display_rows(), 0..1);
}

/// Strip ANSI escape sequences from output for easier assertions.
#[expect(clippy::disallowed_types, reason = "test helper building arbitrary output string")]
fn strip_ansi(s: &str) -> String {
Expand Down
Loading