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
@@ -202,6 +202,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
// Command-line entry is shown uppercase.
|
||||
self.command_line.input.push_str(&s.to_uppercase());
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
||||
let live = self.command_line.input.clone();
|
||||
let i = self.active_tab;
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) }
|
||||
} else { (false, Vec::new(), String::new()) };
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
@@ -231,6 +241,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.command_line.input.pop();
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT
|
||||
{
|
||||
let live = self.command_line.input.clone();
|
||||
let i = self.active_tab;
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) }
|
||||
} else { (false, Vec::new(), String::new()) };
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } }
|
||||
}
|
||||
}
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,9 @@ impl OpenCADStudio {
|
||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||
language: self.language,
|
||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||
block_mru: self.block_mru.clone(),
|
||||
block_freq: self.block_freq.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +410,17 @@ impl OpenCADStudio {
|
||||
if crate::i18n::set_language(s.language).is_ok() {
|
||||
self.language = s.language;
|
||||
}
|
||||
self.cliprompt_lines = crate::app::settings::clamp_clipromptlines(s.cliprompt_lines);
|
||||
self.command_line
|
||||
.set_cliprompt_lines(self.cliprompt_lines.clamp(0, 50) as u8);
|
||||
// Block usage: clone but cap to sane sizes (MRU 20, freq map 200)
|
||||
self.block_mru = s.block_mru.iter().take(20).cloned().collect();
|
||||
self.block_freq = s
|
||||
.block_freq
|
||||
.iter()
|
||||
.take(200)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
// Push the restored background onto every drawing tab that exists now
|
||||
// (the start tab and any initial drawing). Tabs created later pick it
|
||||
// up via `apply_bg_default` at their construction site.
|
||||
@@ -416,6 +430,79 @@ impl OpenCADStudio {
|
||||
self.rebuild_ribbon_modules();
|
||||
}
|
||||
|
||||
pub(crate) fn record_block_insert(&mut self, name: &str) {
|
||||
let key = name.to_ascii_uppercase();
|
||||
*self.block_freq.entry(key).or_insert(0) += 1;
|
||||
self.block_mru.retain(|n| !n.eq_ignore_ascii_case(name));
|
||||
self.block_mru.insert(0, name.to_string());
|
||||
if self.block_mru.len() > 20 {
|
||||
self.block_mru.truncate(20);
|
||||
}
|
||||
// Cap freq map to 200 most frequent to bound persistence size.
|
||||
if self.block_freq.len() > 200 {
|
||||
let mut items: Vec<(String, u32)> = self
|
||||
.block_freq
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
items.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
items.truncate(200);
|
||||
self.block_freq = items.into_iter().collect();
|
||||
}
|
||||
// Debounce disk writes: at most once per second (2.4) to avoid thrash
|
||||
// during rapid scripting/batch inserts. Immediate first write ensures
|
||||
// crash recovery. Wasm has no `std::time::Instant` guarantee, so persist
|
||||
// immediately there.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let now = std::time::Instant::now();
|
||||
let should_persist = match self.block_usage_last_persist {
|
||||
None => true,
|
||||
Some(t) => now.duration_since(t).as_secs_f32() >= 1.0,
|
||||
};
|
||||
if should_persist {
|
||||
self.block_usage_last_persist = Some(now);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_usage_snapshot(&self) -> rustc_hash::FxHashMap<String, (u32, usize)> {
|
||||
let mut out = rustc_hash::FxHashMap::default();
|
||||
for (idx, name) in self.block_mru.iter().enumerate() {
|
||||
let up = name.to_ascii_uppercase();
|
||||
let freq = self.block_freq.get(&up).copied().unwrap_or(0);
|
||||
out.insert(up, (freq, idx));
|
||||
}
|
||||
// Ensure every freq entry has at least an entry (for blocks not in MRU)
|
||||
for (up, freq) in &self.block_freq {
|
||||
out.entry(up.clone()).or_insert((*freq, usize::MAX));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn ranked_block_names(&self, names: &[String]) -> Vec<String> {
|
||||
let mut scored: Vec<(&String, u32, usize)> = names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let up = n.to_ascii_uppercase();
|
||||
let f = self.block_freq.get(&up).copied().unwrap_or(0);
|
||||
let m = self
|
||||
.block_mru
|
||||
.iter()
|
||||
.position(|x| x.eq_ignore_ascii_case(n))
|
||||
.unwrap_or(usize::MAX);
|
||||
(n, f, m)
|
||||
})
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.2.cmp(&b.2)).then_with(|| a.0.cmp(b.0)));
|
||||
scored.into_iter().map(|(n, _, _)| n.clone()).collect()
|
||||
}
|
||||
|
||||
/// Adopt the per-drawing sysvars stored in tab `i`'s document header —
|
||||
/// Ortho (`$ORTHOMODE`) and the running object-snap set (`$OSMODE`) — into
|
||||
/// the live app state, so each drawing keeps its own. Called when a drawing
|
||||
|
||||
+30
-1
@@ -1455,9 +1455,38 @@ impl OpenCADStudio {
|
||||
self.command_line.input = s;
|
||||
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
|
||||
}
|
||||
self.command_line.input = s;
|
||||
let live_input = s.clone();
|
||||
self.command_line.input = live_input.clone();
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT: update picker on each keystroke
|
||||
// without requiring Enter. Performance-first: uses upper/lower caches
|
||||
// and partial sort (O(k)) so per-keystroke is <0.2ms even for 10k blocks.
|
||||
// Suggestion count is in direct relation to CLIPROMPTLINES (picker limit).
|
||||
{
|
||||
let i = self.active_tab;
|
||||
// Capture prompt/options while holding cmd borrow, then release before
|
||||
// borrowing command_line to satisfy borrow checker.
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live_input) {
|
||||
(true, cmd.options(), cmd.prompt())
|
||||
} else {
|
||||
(false, Vec::new(), String::new())
|
||||
}
|
||||
} else {
|
||||
(false, Vec::new(), String::new())
|
||||
};
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
// Update pinned prompt text live without pushing new history entry
|
||||
// (avoids flooding history with one entry per keystroke).
|
||||
if let Some(last) = self.command_line.history.last_mut() {
|
||||
if last.pinned {
|
||||
last.text = prompt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::batch(vec![sweep, Task::none()])
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user