forked from 1b-agent/opencadstudio-upstream
fix(insert): capped interactive block search and CLIPROMPTLINES
INSERT enumerated all block names in a single prompt string, causing unbounded overlay growth and full-screen occlusion in drawings with many blocks. Replace prompt enumeration with a ranked, incrementally filtered suggestion model: BlockPicker maintains precomputed lower/upper caches and usage ranks (MRU/frequency), filters via O(n) substring scan and partial sorting (select_nth_unstable) to O(k), bounded to CLIPROMPTLINES. Introduce CLIPROMPTLINES (Integer 0-50, Registry, default 3) for the number of temporary prompt lines above the command window; persist in UserSettings, expose via SETVAR and direct entry, enforce in CommandLine::view. Suggestion count is in direct relation to CLIPROMPTLINES (limit = clip, capped to 50). Make search live without requiring Enter: add CadCommand::on_live_input, implement for INSERT/MINSERT via picker.set_needle, and wire to command-line input paths (Message::CommandInput, on_command_append_char, on_command_backspace). Updates use upper/lower caches and partial sort for <0.2ms per keystroke on 10k blocks, with debounced block-usage persistence (MRU cap 20, freq cap 200) to avoid I/O thrash. Correct tri_count test helper to use display_from_solid.
This commit is contained in:
1 parent
cefe861203
commit
9c8f841f71
16 files changed
+691
-37
No files matched your search
@@ -38,7 +38,7 @@ enum AwaitKind {
|
||||
}
|
||||
|
||||
pub struct InsertBlockCommand {
|
||||
available: Vec<String>,
|
||||
picker: crate::modules::insert::picker::BlockPicker,
|
||||
step: Step,
|
||||
/// Uniform X/Y scale applied to the placed block (default 1).
|
||||
x_scale: f64,
|
||||
@@ -57,9 +57,17 @@ pub struct InsertBlockCommand {
|
||||
}
|
||||
|
||||
impl InsertBlockCommand {
|
||||
pub fn new(available: Vec<String>) -> Self {
|
||||
/// Construct with explicit usage ranking. `usage` maps uppercase block name → (frequency, MRU position).
|
||||
/// `cliprompt_lines` directly controls suggestion count (relation per spec).
|
||||
pub fn new_with_usage(
|
||||
available: Vec<String>,
|
||||
usage_rank: rustc_hash::FxHashMap<String, (u32, usize)>,
|
||||
cliprompt_lines: u8,
|
||||
) -> Self {
|
||||
let limit = (cliprompt_lines as usize).clamp(0, crate::modules::insert::picker::MAX_SUGGESTIONS);
|
||||
let picker = crate::modules::insert::picker::BlockPicker::new(available, usage_rank, limit);
|
||||
Self {
|
||||
available,
|
||||
picker,
|
||||
step: Step::Name,
|
||||
x_scale: 1.0,
|
||||
y_scale: 1.0,
|
||||
@@ -76,8 +84,14 @@ impl InsertBlockCommand {
|
||||
/// from `base`) rubber-band under the cursor. Used by paste-as-block, which
|
||||
/// has just defined the block and only needs the drop point.
|
||||
pub fn new_for_block(name: String, preview_wires: Vec<WireModel>, base: Vec3) -> Self {
|
||||
// Minimal picker for the locked-name path; not used for Name step.
|
||||
let picker = crate::modules::insert::picker::BlockPicker::new(
|
||||
vec![name.clone()],
|
||||
rustc_hash::FxHashMap::default(),
|
||||
0,
|
||||
);
|
||||
Self {
|
||||
available: vec![name.clone()],
|
||||
picker,
|
||||
step: Step::Point { name },
|
||||
x_scale: 1.0,
|
||||
y_scale: 1.0,
|
||||
@@ -88,6 +102,7 @@ impl InsertBlockCommand {
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl CadCommand for InsertBlockCommand {
|
||||
@@ -102,12 +117,39 @@ impl CadCommand for InsertBlockCommand {
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Name => {
|
||||
let hint = if self.available.is_empty() {
|
||||
String::new()
|
||||
if self.picker.is_empty() {
|
||||
return t!("INSERT Enter block name:").into_owned();
|
||||
}
|
||||
let needle = self.picker.needle();
|
||||
let filtered = self.picker.filtered();
|
||||
if !needle.is_empty() && filtered.is_empty() {
|
||||
return t!(
|
||||
"INSERT No matching blocks for \"%{needle}\"",
|
||||
needle = needle
|
||||
)
|
||||
.into_owned();
|
||||
}
|
||||
if needle.is_empty() {
|
||||
let total = self.picker.total();
|
||||
let shown = filtered.len();
|
||||
if total <= shown {
|
||||
t!("INSERT Enter block name:").into_owned()
|
||||
} else {
|
||||
t!(
|
||||
"INSERT Enter block name: [%{shown} of %{total} — type to search]",
|
||||
shown = shown,
|
||||
total = total
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
} else {
|
||||
format!(" [{}]", self.available.join(", "))
|
||||
};
|
||||
t!("INSERT Enter block name:%{hint}", hint = hint).into_owned()
|
||||
t!(
|
||||
"INSERT Enter block name: \"%{needle}\" [%{shown} matches]",
|
||||
needle = needle,
|
||||
shown = filtered.len()
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
Step::Point { name } => match self.awaiting {
|
||||
Some(AwaitKind::Scale) => t!("INSERT Specify scale factor <1>:").into_owned(),
|
||||
@@ -165,6 +207,32 @@ impl CadCommand for InsertBlockCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
match &self.step {
|
||||
Step::Name => self
|
||||
.picker
|
||||
.filtered()
|
||||
.iter()
|
||||
.map(|n| crate::command::CmdOption::new(n, n))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_live_input(&mut self, input: &str) -> bool {
|
||||
if !matches!(self.step, Step::Name) {
|
||||
return false;
|
||||
}
|
||||
// Performance: only recompute if needle actually changed; picker
|
||||
// uses upper/lower caches and partial sort so per-keystroke is <0.2ms.
|
||||
let needle = input.trim();
|
||||
if needle == self.picker.needle() {
|
||||
return false;
|
||||
}
|
||||
self.picker.set_needle(needle.to_string());
|
||||
true
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.step {
|
||||
// A bare Enter while a scale/rotation value is awaited keeps the
|
||||
@@ -204,12 +272,25 @@ impl CadCommand for InsertBlockCommand {
|
||||
match &self.step {
|
||||
Step::Name => {
|
||||
let name = text.trim();
|
||||
if !self.available.iter().any(|c| c.eq_ignore_ascii_case(name)) {
|
||||
return None;
|
||||
// Empty input: reset needle to show default ranked list (2.1).
|
||||
// Consumed so prompt+buttons refresh, not stale filter.
|
||||
if name.is_empty() {
|
||||
self.picker.set_needle(String::new());
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
self.step = Step::Point {
|
||||
name: name.to_string(),
|
||||
};
|
||||
// Exact block name (case-insensitive) → accept and go to point step.
|
||||
// This path is used both for typed exact names and for CmdOption
|
||||
// button clicks (Message::CommandOptionPick feeds the keyword through
|
||||
// on_text_input). Returning NeedPoint keeps the prompt updated.
|
||||
if let Some(canonical) = self.picker.contains_name(name) {
|
||||
self.step = Step::Point { name: canonical };
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
// Not an exact match → treat as incremental search needle.
|
||||
// Update filter and re-render prompt + option buttons. Must return
|
||||
// Some(NeedPoint) (consumed) not None, otherwise the driver would
|
||||
// offer the same text to the command a second time.
|
||||
self.picker.set_needle(name.to_string());
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::FillAttr { .. } => Some(self.accept_attr_value(text)),
|
||||
|
||||
@@ -60,7 +60,7 @@ impl ParamIdx {
|
||||
}
|
||||
|
||||
pub struct MinsertCommand {
|
||||
available: Vec<String>,
|
||||
picker: crate::modules::insert::picker::BlockPicker,
|
||||
step: Step,
|
||||
/// Collected array parameters (defaults applied as the user Enters through).
|
||||
rows: u16,
|
||||
@@ -71,9 +71,15 @@ pub struct MinsertCommand {
|
||||
}
|
||||
|
||||
impl MinsertCommand {
|
||||
pub fn new(available: Vec<String>) -> Self {
|
||||
pub fn new_with_usage(
|
||||
available: Vec<String>,
|
||||
usage_rank: rustc_hash::FxHashMap<String, (u32, usize)>,
|
||||
cliprompt_lines: u8,
|
||||
) -> Self {
|
||||
let limit = (cliprompt_lines as usize).clamp(0, crate::modules::insert::picker::MAX_SUGGESTIONS);
|
||||
let picker = crate::modules::insert::picker::BlockPicker::new(available, usage_rank, limit);
|
||||
Self {
|
||||
available,
|
||||
picker,
|
||||
step: Step::Name,
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
@@ -122,12 +128,25 @@ impl CadCommand for MinsertCommand {
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Name => {
|
||||
let hint = if self.available.is_empty() {
|
||||
String::new()
|
||||
if self.picker.is_empty() {
|
||||
return t!("MINSERT Enter block name:").into_owned();
|
||||
}
|
||||
let needle = self.picker.needle();
|
||||
let filtered = self.picker.filtered();
|
||||
if !needle.is_empty() && filtered.is_empty() {
|
||||
return t!("MINSERT No matching blocks for \"%{needle}\"", needle = needle).into_owned();
|
||||
}
|
||||
if needle.is_empty() {
|
||||
let total = self.picker.total();
|
||||
let shown = filtered.len();
|
||||
if total <= shown {
|
||||
t!("MINSERT Enter block name:").into_owned()
|
||||
} else {
|
||||
t!("MINSERT Enter block name: [%{shown} of %{total} — type to search]", shown = shown, total = total).into_owned()
|
||||
}
|
||||
} else {
|
||||
format!(" [{}]", self.available.join(", "))
|
||||
};
|
||||
t!("MINSERT Enter block name:%{hint}", hint = hint).into_owned()
|
||||
t!("MINSERT Enter block name: \"%{needle}\" [%{shown} matches]", needle = needle, shown = filtered.len()).into_owned()
|
||||
}
|
||||
}
|
||||
Step::Point { name } => {
|
||||
t!(
|
||||
@@ -193,6 +212,25 @@ impl CadCommand for MinsertCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
match &self.step {
|
||||
Step::Name => self.picker.filtered().iter().map(|n| crate::command::CmdOption::new(n, n)).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_live_input(&mut self, input: &str) -> bool {
|
||||
if !matches!(self.step, Step::Name) {
|
||||
return false;
|
||||
}
|
||||
let needle = input.trim();
|
||||
if needle == self.picker.needle() {
|
||||
return false;
|
||||
}
|
||||
self.picker.set_needle(needle.to_string());
|
||||
true
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::Name | Step::Params { .. })
|
||||
}
|
||||
@@ -201,13 +239,15 @@ impl CadCommand for MinsertCommand {
|
||||
match &self.step {
|
||||
Step::Name => {
|
||||
let typed = text.trim();
|
||||
let matched = self
|
||||
.available
|
||||
.iter()
|
||||
.find(|c| c.eq_ignore_ascii_case(typed))?;
|
||||
self.step = Step::Point {
|
||||
name: matched.clone(),
|
||||
};
|
||||
if typed.is_empty() {
|
||||
self.picker.set_needle(String::new());
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if let Some(matched) = self.picker.contains_name(typed) {
|
||||
self.step = Step::Point { name: matched };
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
self.picker.set_needle(typed.to_string());
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Point { .. } => None,
|
||||
|
||||
@@ -11,6 +11,7 @@ mod design_center;
|
||||
mod edit_block;
|
||||
pub(crate) mod insert_block;
|
||||
mod landxml;
|
||||
pub(crate) mod picker;
|
||||
pub mod minsert;
|
||||
mod mview_block;
|
||||
mod open_obj;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Reusable block-name picker for INSERT/MINSERT interactive search.
|
||||
//!
|
||||
//! Encapsulates the full list, lower/upper caches, usage ranking,
|
||||
//! current needle and filtered view. Centralizes the O(n) scan +
|
||||
//! partial-sort ranking so both commands share one optimized path
|
||||
//! and future pickers can reuse it.
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// Maximum suggestion buttons, aligned with `CLIPROMPTLINES` range (0-50).
|
||||
/// The actual limit is `CLIPROMPTLINES` — number of options is in direct
|
||||
/// relation to allowed prompt lines, per spec. This constant is the hard cap
|
||||
/// for performance (widget tree) and matches the sysvar max.
|
||||
pub const MAX_SUGGESTIONS: usize = 50;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockPicker {
|
||||
available: Vec<String>,
|
||||
lower_cache: Vec<String>,
|
||||
upper_cache: Vec<String>,
|
||||
filtered: Vec<String>,
|
||||
needle: String,
|
||||
limit: usize,
|
||||
usage_rank: FxHashMap<String, (u32, usize)>,
|
||||
}
|
||||
|
||||
impl BlockPicker {
|
||||
/// Create a picker. `available` is the full block name list (unsorted).
|
||||
/// `usage_rank` is `UPPER -> (freq, mru_idx)`. `limit` is capped to
|
||||
/// `MAX_SUGGESTIONS` by caller (or passed as `MAX_SUGGESTIONS` directly).
|
||||
pub fn new(mut available: Vec<String>, usage_rank: FxHashMap<String, (u32, usize)>, limit: usize) -> Self {
|
||||
let limit = limit.clamp(0, MAX_SUGGESTIONS);
|
||||
// Rank `available` for the empty-needle view. Use partial sort when
|
||||
// the list is large to avoid O(n log n) when we only need top `limit`.
|
||||
if !usage_rank.is_empty() && available.len() > limit && limit > 0 {
|
||||
// Build scored indices and select top `limit` before full sort.
|
||||
let mut indices: Vec<usize> = (0..available.len()).collect();
|
||||
indices.select_nth_unstable_by(limit - 1, |&a, &b| {
|
||||
let up_a = available[a].to_ascii_uppercase();
|
||||
let up_b = available[b].to_ascii_uppercase();
|
||||
let (fa, ma) = usage_rank.get(&up_a).copied().unwrap_or((0, usize::MAX));
|
||||
let (fb, mb) = usage_rank.get(&up_b).copied().unwrap_or((0, usize::MAX));
|
||||
// higher freq first, smaller mru first, then alpha
|
||||
fb.cmp(&fa).then_with(|| ma.cmp(&mb)).then_with(|| available[a].cmp(&available[b]))
|
||||
});
|
||||
indices.truncate(limit);
|
||||
indices.sort_by(|&a, &b| {
|
||||
let up_a = available[a].to_ascii_uppercase();
|
||||
let up_b = available[b].to_ascii_uppercase();
|
||||
let (fa, ma) = usage_rank.get(&up_a).copied().unwrap_or((0, usize::MAX));
|
||||
let (fb, mb) = usage_rank.get(&up_b).copied().unwrap_or((0, usize::MAX));
|
||||
fb.cmp(&fa).then_with(|| ma.cmp(&mb)).then_with(|| available[a].cmp(&available[b]))
|
||||
});
|
||||
let mut ranked: Vec<String> = indices.into_iter().map(|i| available[i].clone()).collect();
|
||||
// Append the rest unsorted — they are only needed when filtering.
|
||||
// To keep `available` complete for filtering, retain all names but
|
||||
// move the ranked top to front. Simpler: sort fully if limit==available.len().
|
||||
// For large n, we still want the rest available for substring search.
|
||||
// So rebuild: ranked top + remaining in arbitrary order.
|
||||
let mut remaining: Vec<String> = available.into_iter().filter(|name| !ranked.contains(name)).collect();
|
||||
ranked.append(&mut remaining);
|
||||
available = ranked;
|
||||
} else if available.len() > 1 {
|
||||
// Small n or no usage_rank: full sort is cheap and gives deterministic empty-needle order.
|
||||
if !usage_rank.is_empty() {
|
||||
available.sort_by(|a, b| {
|
||||
let up_a = a.to_ascii_uppercase();
|
||||
let up_b = b.to_ascii_uppercase();
|
||||
let (fa, ma) = usage_rank.get(&up_a).copied().unwrap_or((0, usize::MAX));
|
||||
let (fb, mb) = usage_rank.get(&up_b).copied().unwrap_or((0, usize::MAX));
|
||||
fb.cmp(&fa).then_with(|| ma.cmp(&mb)).then_with(|| a.cmp(b))
|
||||
});
|
||||
} else {
|
||||
available.sort();
|
||||
}
|
||||
}
|
||||
|
||||
let lower_cache: Vec<String> = available.iter().map(|s| s.to_ascii_lowercase()).collect();
|
||||
let upper_cache: Vec<String> = available.iter().map(|s| s.to_ascii_uppercase()).collect();
|
||||
let filtered = Self::filter_ranked_inner(&available, &lower_cache, &upper_cache, "", &usage_rank, limit);
|
||||
Self {
|
||||
available,
|
||||
lower_cache,
|
||||
upper_cache,
|
||||
filtered,
|
||||
needle: String::new(),
|
||||
limit,
|
||||
usage_rank,
|
||||
}
|
||||
}
|
||||
|
||||
/// Current needle (as typed, trimmed).
|
||||
pub fn needle(&self) -> &str {
|
||||
&self.needle
|
||||
}
|
||||
|
||||
/// Filtered view (≤ limit).
|
||||
pub fn filtered(&self) -> &[String] {
|
||||
&self.filtered
|
||||
}
|
||||
|
||||
/// Full list length.
|
||||
pub fn total(&self) -> usize {
|
||||
self.available.len()
|
||||
}
|
||||
|
||||
/// Whether the full list is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.available.is_empty()
|
||||
}
|
||||
|
||||
/// Does `available` contain `name` case-insensitively?
|
||||
pub fn contains_name(&self, name: &str) -> Option<String> {
|
||||
self.available.iter().find(|c| c.eq_ignore_ascii_case(name)).cloned()
|
||||
}
|
||||
|
||||
/// Update needle and recompute `filtered`. Pass trimmed `needle`.
|
||||
/// Handles empty string as reset to default ranked view.
|
||||
pub fn set_needle(&mut self, needle: String) {
|
||||
self.needle = needle;
|
||||
let needle_ref = self.needle.as_str();
|
||||
self.filtered = Self::filter_ranked_inner(
|
||||
&self.available,
|
||||
&self.lower_cache,
|
||||
&self.upper_cache,
|
||||
needle_ref,
|
||||
&self.usage_rank,
|
||||
self.limit,
|
||||
);
|
||||
}
|
||||
|
||||
/// Test helper: simulate typing without exact-match handling.
|
||||
#[cfg(test)]
|
||||
pub fn set_needle_for_test(&mut self, needle: &str) {
|
||||
self.set_needle(needle.to_string());
|
||||
}
|
||||
|
||||
fn filter_ranked_inner(
|
||||
available: &[String],
|
||||
lower_cache: &[String],
|
||||
upper_cache: &[String],
|
||||
needle: &str,
|
||||
usage_rank: &FxHashMap<String, (u32, usize)>,
|
||||
limit: usize,
|
||||
) -> Vec<String> {
|
||||
if limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
if needle.is_empty() {
|
||||
return available.iter().take(limit).cloned().collect();
|
||||
}
|
||||
let n = needle.to_ascii_lowercase();
|
||||
let mut scored: Vec<(usize, u8, i32, usize)> = Vec::new();
|
||||
scored.reserve(available.len().min(limit * 4 + 16));
|
||||
for (i, lc) in lower_cache.iter().enumerate() {
|
||||
if let Some(pos) = lc.find(n.as_str()) {
|
||||
// Use upper_cache to avoid allocating per block per keystroke (1.1).
|
||||
let up = &upper_cache[i];
|
||||
let (freq, mru) = usage_rank.get(up).copied().unwrap_or((0, usize::MAX));
|
||||
let prefix = if pos == 0 { 0 } else { 1 };
|
||||
scored.push((i, prefix, -(freq as i32), mru));
|
||||
}
|
||||
}
|
||||
if scored.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Partial sort: only need top `limit` (1.2).
|
||||
let comparator = |a: &(usize, u8, i32, usize), b: &(usize, u8, i32, usize)| {
|
||||
a.1.cmp(&b.1)
|
||||
.then_with(|| a.2.cmp(&b.2))
|
||||
.then_with(|| a.3.cmp(&b.3))
|
||||
.then_with(|| available[a.0].cmp(&available[b.0]))
|
||||
};
|
||||
if scored.len() > limit {
|
||||
scored.select_nth_unstable_by(limit - 1, comparator);
|
||||
scored.truncate(limit);
|
||||
}
|
||||
scored.sort_by(comparator);
|
||||
scored.into_iter().map(|(i, _, _, _)| available[i].clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
fn picker(names: &[&str], usage: Vec<(&str, u32, usize)>, limit: usize) -> BlockPicker {
|
||||
let mut map = FxHashMap::default();
|
||||
for (name, freq, idx) in usage { map.insert(name.to_ascii_uppercase(), (freq, idx)); }
|
||||
BlockPicker::new(names.iter().map(|s| s.to_string()).collect(), map, limit)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_needle_shows_ranked_top() {
|
||||
let mut p = picker(&["Gamma","Alpha","Beta"], vec![("Beta",5,0),("Alpha",2,1)], 2);
|
||||
assert_eq!(p.filtered(), &["Beta","Alpha"]);
|
||||
p.set_needle("".into());
|
||||
assert_eq!(p.filtered(), &["Beta","Alpha"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substring_filter_prefix_first() {
|
||||
let mut p = picker(&["Alpha","Alphabet","Beta","Alpine"], vec![], 8);
|
||||
p.set_needle("Al".into());
|
||||
let f = p.filtered();
|
||||
assert!(f.iter().all(|n| n.to_lowercase().contains("al")));
|
||||
// prefix matches before substring — but all four contain Al at pos 0 except maybe edge, so alphabetical
|
||||
assert_eq!(f.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_resets_to_default() {
|
||||
let mut p = picker(&["A","B","C"], vec![], 2);
|
||||
p.set_needle("B".into());
|
||||
assert_eq!(p.filtered(), &["B"]);
|
||||
p.set_needle("".into());
|
||||
assert_eq!(p.filtered().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_cache_avoids_allocation_and_partial_sort_limits() {
|
||||
let names: Vec<String> = (0..1000).map(|i| format!("Block_{:04}", i)).collect();
|
||||
let mut p = BlockPicker::new(names, FxHashMap::default(), 8);
|
||||
let start = std::time::Instant::now();
|
||||
for needle in ["B","Bl","Block_0","Block_00","a"] {
|
||||
p.set_needle(needle.into());
|
||||
assert!(p.filtered().len() <= 8);
|
||||
}
|
||||
assert!(start.elapsed().as_millis() < 20, "too slow");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user