Skip to content

Commit f437b0a

Browse files
committed
feat: add before_render hook to select_list and use nearest-package detection for current-package prioritization
1 parent 8a1a73f commit f437b0a

10 files changed

Lines changed: 283 additions & 39 deletions

File tree

crates/vite_select/src/interactive.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub fn run(
238238
selected_index: &mut usize,
239239
header: Option<&str>,
240240
page_size: usize,
241+
mut before_render: impl FnMut(&mut Vec<usize>, &str),
241242
mut after_render: impl FnMut(&RenderState<'_>),
242243
) -> anyhow::Result<()> {
243244
if items.is_empty() {
@@ -250,6 +251,7 @@ pub fn run(
250251
crossterm::execute!(out, cursor::Hide)?;
251252

252253
let mut state = State::new(items, initial_query, page_size);
254+
before_render(&mut state.filtered, &state.query);
253255

254256
// Initial render
255257
render(&mut out, &mut state, header)?;
@@ -263,6 +265,7 @@ pub fn run(
263265
// Clear the search query and reset the filter
264266
state.query.clear();
265267
state.refilter();
268+
before_render(&mut state.filtered, &state.query);
266269
}
267270
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
268271
cleanup(&mut out, &state)?;
@@ -284,10 +287,12 @@ pub fn run(
284287
KeyCode::Char(c) => {
285288
state.query.push(c);
286289
state.refilter();
290+
before_render(&mut state.filtered, &state.query);
287291
}
288292
KeyCode::Backspace => {
289293
state.query.pop();
290294
state.refilter();
295+
before_render(&mut state.filtered, &state.query);
291296
}
292297
_ => continue,
293298
},

crates/vite_select/src/lib.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ pub struct RenderState<'a> {
3434
pub selected_index: usize,
3535
}
3636

37+
/// Parameters for [`select_list`].
38+
pub struct SelectParams<'a> {
39+
pub items: &'a [SelectItem],
40+
/// Initial search query (pre-filled in interactive, used as filter in non-interactive).
41+
pub query: Option<&'a str>,
42+
/// Header line rendered above the list (e.g. an error message).
43+
pub header: Option<&'a str>,
44+
/// Max visible rows (interactive only).
45+
pub page_size: usize,
46+
}
47+
3748
/// Show a task selection list.
3849
///
3950
/// In [`Mode::Interactive`], enters a terminal UI with fuzzy search and
@@ -50,18 +61,24 @@ pub struct RenderState<'a> {
5061
/// Returns an error if terminal I/O fails.
5162
pub fn select_list(
5263
writer: &mut impl Write,
53-
items: &[SelectItem],
54-
query: Option<&str>,
64+
params: &SelectParams<'_>,
5565
mode: Mode<'_>,
56-
header: Option<&str>,
57-
page_size: usize,
66+
before_render: impl FnMut(&mut Vec<usize>, &str),
5867
after_render: impl FnMut(&RenderState<'_>),
5968
) -> anyhow::Result<()> {
6069
match mode {
61-
Mode::Interactive { selected_index } => {
62-
interactive::run(items, query, selected_index, header, page_size, after_render)
70+
Mode::Interactive { selected_index } => interactive::run(
71+
params.items,
72+
params.query,
73+
selected_index,
74+
params.header,
75+
params.page_size,
76+
before_render,
77+
after_render,
78+
),
79+
Mode::NonInteractive => {
80+
non_interactive(writer, params.items, params.query, params.header, before_render)
6381
}
64-
Mode::NonInteractive => non_interactive(writer, items, query, header),
6582
}
6683
}
6784

@@ -70,10 +87,12 @@ fn non_interactive(
7087
items: &[SelectItem],
7188
query: Option<&str>,
7289
header: Option<&str>,
90+
mut before_render: impl FnMut(&mut Vec<usize>, &str),
7391
) -> anyhow::Result<()> {
7492
let labels: Vec<&str> = items.iter().map(|item| item.label.as_str()).collect();
75-
let filtered: Vec<usize> =
93+
let mut filtered: Vec<usize> =
7694
query.map_or_else(|| (0..items.len()).collect(), |q| fuzzy_match(q, &labels));
95+
before_render(&mut filtered, query.unwrap_or_default());
7796
let len = filtered.len();
7897

7998
render_items(

crates/vite_task/src/session/mod.rs

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ impl<'a> Session<'a> {
300300
) -> anyhow::Result<ExitStatus> {
301301
let cwd = Arc::clone(&self.cwd);
302302
let task_graph = self.ensure_task_graph_loaded().await?;
303+
let current_package_path = task_graph.get_package_path_from_cwd(&cwd).cloned();
303304
let mut entries = task_graph.list_tasks();
304305
entries.sort_unstable_by(|a, b| {
305306
a.task_display
@@ -308,31 +309,19 @@ impl<'a> Session<'a> {
308309
.then_with(|| a.task_display.task_name.cmp(&b.task_display.task_name))
309310
});
310311

311-
// Find the most specific package containing the CWD (longest matching path)
312-
let current_package_path = entries
312+
// Build items: current package tasks use unqualified names (no '#'),
313+
// other packages use qualified "package#task" names.
314+
let select_items: Vec<SelectItem> = entries
313315
.iter()
314-
.map(|e| &e.task_display.package_path)
315-
.filter(|p| cwd.as_path().starts_with(p.as_path()))
316-
.max_by_key(|p| p.as_path().as_os_str().len())
317-
.cloned();
318-
319-
// Sort: current package tasks first, then others
320-
let (current, others): (Vec<_>, Vec<_>) = entries
321-
.iter()
322-
.partition(|e| current_package_path.as_ref() == Some(&e.task_display.package_path));
323-
324-
// Build the items list: current package tasks first (unqualified name),
325-
// then other packages (qualified with package#task).
326-
let select_items: Vec<SelectItem> = current
327-
.iter()
328-
.map(|entry| SelectItem {
329-
label: entry.task_display.task_name.clone(),
330-
description: entry.command.clone(),
316+
.map(|entry| {
317+
let label =
318+
if current_package_path.as_ref() == Some(&entry.task_display.package_path) {
319+
entry.task_display.task_name.clone()
320+
} else {
321+
vite_str::format!("{}", entry.task_display)
322+
};
323+
SelectItem { label, description: entry.command.clone() }
331324
})
332-
.chain(others.iter().map(|entry| SelectItem {
333-
label: vite_str::format!("{}", entry.task_display),
334-
description: entry.command.clone(),
335-
}))
336325
.collect();
337326

338327
// Build header: interactive says "not found.", non-interactive "did you mean:" suffix
@@ -347,19 +336,30 @@ impl<'a> Session<'a> {
347336
// Build mode-dependent params and call select_list once
348337
let mut selected_index = if is_interactive { Some(0) } else { None };
349338
let mut stdout = std::io::stdout();
350-
let mode = if let Some(selected_index) = selected_index.as_mut() {
351-
vite_select::Mode::Interactive { selected_index }
352-
} else {
353-
vite_select::Mode::NonInteractive
339+
let mode =
340+
selected_index.as_mut().map_or(vite_select::Mode::NonInteractive, |selected_index| {
341+
vite_select::Mode::Interactive { selected_index }
342+
});
343+
344+
let params = vite_select::SelectParams {
345+
items: &select_items,
346+
query: not_found_name,
347+
header: header.as_deref(),
348+
page_size: 8,
354349
};
355350

356351
vite_select::select_list(
357352
&mut stdout,
358-
&select_items,
359-
not_found_name,
353+
&params,
360354
mode,
361-
header.as_deref(),
362-
8,
355+
|filtered, query| {
356+
// When the query doesn't contain '#', move current-package tasks (those
357+
// without '#' in their label) to the top. `sort_by_key` is a stable sort,
358+
// so the fuzzy rating order is preserved within each group.
359+
if !query.contains('#') {
360+
filtered.sort_by_key(|&idx| select_items[idx].label.contains('#'));
361+
}
362+
},
363363
|state| {
364364
use std::io::Write;
365365
let milestone_name =

crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,46 @@ steps = [
7777
{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "write-key" = "up" },{ "expect-milestone" = "task-select::0" },{ "write-key" = "enter" },] },
7878
]
7979

80+
# Non-interactive: list tasks from lib package (lib tasks first, unqualified)
81+
[[e2e]]
82+
name = "non-interactive list tasks from lib"
83+
cwd = "packages/lib"
84+
steps = [
85+
"echo '' | vp run",
86+
]
87+
88+
# Interactive: select from lib package (first item is lib's task)
89+
[[e2e]]
90+
name = "interactive select task from lib"
91+
cwd = "packages/lib"
92+
steps = [
93+
{ command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write-key" = "enter" }] },
94+
]
95+
96+
# Interactive: search for a task that only exists in another package
97+
[[e2e]]
98+
name = "interactive search other package task"
99+
cwd = "packages/app"
100+
steps = [
101+
{ command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write" = "typec" }, { "expect-milestone" = "task-select:typec:0" }, { "write-key" = "enter" }] },
102+
]
103+
104+
# Interactive: '#' in query skips current-package reordering
105+
[[e2e]]
106+
name = "interactive search with hash skips reorder"
107+
cwd = "packages/app"
108+
steps = [
109+
{ command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write" = "lib#" }, { "expect-milestone" = "task-select:lib#:0" }, { "write-key" = "enter" }] },
110+
]
111+
112+
# Interactive: multiple current-package matches preserve fuzzy rating order
113+
[[e2e]]
114+
name = "interactive search preserves rating within package"
115+
cwd = "packages/lib"
116+
steps = [
117+
{ command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write" = "t" }, { "expect-milestone" = "task-select:t:0" }, { "write-key" = "enter" }] },
118+
]
119+
80120
# Typo inside a task script should fail with an error, NOT show a list
81121
[[e2e]]
82122
name = "typo in task script fails without list"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run
6+
@ expect-milestone: task-select::0
7+
Search task (↑/to move, enter to select):
8+
> build: echo build app
9+
lint: echo lint app
10+
test: echo test app
11+
lib#build: echo build lib
12+
lib#lint: echo lint lib
13+
lib#test: echo test lib
14+
lib#typecheck: echo typecheck lib
15+
task-select-test#check: echo check root
16+
(…4 more)
17+
@ write: typec
18+
@ expect-milestone: task-select:typec:0
19+
Search task (↑/to move, enter to select): typec
20+
> lib#typecheck: echo typecheck lib
21+
@ write-key: enter
22+
~/packages/lib$ echo typecheck libcache disabled: built-in command
23+
typecheck lib
24+
25+
26+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27+
Vite+ Task RunnerExecution Summary
28+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29+
30+
Statistics: 1 tasks0 cache hits0 cache misses1 cache disabled
31+
Performance: 0% cache hit rate
32+
33+
Task Details:
34+
────────────────────────────────────────────────
35+
[1] lib#typecheck: ~/packages/lib$ echo typecheck lib
36+
Cache disabled for built-in command
37+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run
6+
@ expect-milestone: task-select::0
7+
Search task (↑/to move, enter to select):
8+
> build: echo build lib
9+
lint: echo lint lib
10+
test: echo test lib
11+
typecheck: echo typecheck lib
12+
app#build: echo build app
13+
app#lint: echo lint app
14+
app#test: echo test app
15+
task-select-test#check: echo check root
16+
(…4 more)
17+
@ write: t
18+
@ expect-milestone: task-select:t:0
19+
Search task (↑/to move, enter to select): t
20+
> test: echo test lib
21+
typecheck: echo typecheck lib
22+
lint: echo lint lib
23+
task-select-test#check: echo check root
24+
task-select-test#format: echo format root
25+
task-select-test#hello: echo hello from root
26+
task-select-test#run-typo-task: vp run nonexistent-xyz
27+
task-select-test#validate: echo validate root
28+
(…2 more)
29+
@ write-key: enter
30+
~/packages/lib$ echo test libcache disabled: built-in command
31+
test lib
32+
33+
34+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35+
Vite+ Task RunnerExecution Summary
36+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
37+
38+
Statistics: 1 tasks0 cache hits0 cache misses1 cache disabled
39+
Performance: 0% cache hit rate
40+
41+
Task Details:
42+
────────────────────────────────────────────────
43+
[1] lib#test: ~/packages/lib$ echo test lib
44+
Cache disabled for built-in command
45+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run
6+
@ expect-milestone: task-select::0
7+
Search task (↑/to move, enter to select):
8+
> build: echo build app
9+
lint: echo lint app
10+
test: echo test app
11+
lib#build: echo build lib
12+
lib#lint: echo lint lib
13+
lib#test: echo test lib
14+
lib#typecheck: echo typecheck lib
15+
task-select-test#check: echo check root
16+
(…4 more)
17+
@ write: lib#
18+
@ expect-milestone: task-select:lib#:0
19+
Search task (↑/to move, enter to select): lib#
20+
> lib#build: echo build lib
21+
lib#lint: echo lint lib
22+
lib#test: echo test lib
23+
lib#typecheck: echo typecheck lib
24+
@ write-key: enter
25+
~/packages/lib$ echo build libcache disabled: built-in command
26+
build lib
27+
28+
29+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30+
Vite+ Task RunnerExecution Summary
31+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32+
33+
Statistics: 1 tasks0 cache hits0 cache misses1 cache disabled
34+
Performance: 0% cache hit rate
35+
36+
Task Details:
37+
────────────────────────────────────────────────
38+
[1] lib#build: ~/packages/lib$ echo build lib
39+
Cache disabled for built-in command
40+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run
6+
@ expect-milestone: task-select::0
7+
Search task (↑/to move, enter to select):
8+
> build: echo build lib
9+
lint: echo lint lib
10+
test: echo test lib
11+
typecheck: echo typecheck lib
12+
app#build: echo build app
13+
app#lint: echo lint app
14+
app#test: echo test app
15+
task-select-test#check: echo check root
16+
(…4 more)
17+
@ write-key: enter
18+
~/packages/lib$ echo build libcache disabled: built-in command
19+
build lib
20+
21+
22+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
Vite+ Task RunnerExecution Summary
24+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25+
26+
Statistics: 1 tasks0 cache hits0 cache misses1 cache disabled
27+
Performance: 0% cache hit rate
28+
29+
Task Details:
30+
────────────────────────────────────────────────
31+
[1] lib#build: ~/packages/lib$ echo build lib
32+
Cache disabled for built-in command
33+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

0 commit comments

Comments
 (0)