feat(commandline): autocomplete suggestions + inventory-based registry

Type-as-you-go suggestion popup above the input that filters known
command names by substring (so `LEADER` surfaces both LEADER and
MLEADER). Up / Down navigates the list, Enter dispatches the
highlighted entry, clicking a row dispatches directly. Esc on a
non-empty input clears the buffer instead of escalating to
command-cancel.

The name corpus comes from `inventory::submit!` blocks placed next
to each `CadCommand` impl rather than a hand-maintained static list.
Adding a new interactive command means dropping one `inventory::submit!`
line in its own module file — the autocomplete picks it up at the
next compile. 98 submissions seeded across 60 module files;
`command::all_registered_command_names()` flattens them at runtime.

One-shot dispatch arms in `app/commands.rs` (NEW, OPEN, SAVE, UNDO,
…) don't have a `CadCommand` impl and are therefore absent from
autocomplete by design.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-26 22:45:54 +03:00
1 parent 4124cb2689
commit 05ea31aa2a
67 files changed
+507 -2

No files matched your search

+3
View File
@@ -357,6 +357,9 @@ pub enum Message {
CommandHistoryNext,
/// Toggle the dropdown listing the full command-line history.
CommandHistoryToggle,
/// User clicked an autocomplete suggestion — fill the input with
/// the chosen command name and dispatch it.
CommandSuggestionPick(String),
ToggleLayers,
LayerToggleVisible(usize),
LayerToggleLock(usize),
+47
View File
@@ -824,6 +824,9 @@ impl OpenCADStudio {
return Task::done(Message::CommandSubmit);
}
self.command_line.input = s;
// Typing invalidates the previous arrow-key cursor —
// the matches list has likely changed.
self.command_line.autocomplete_cursor = None;
Task::none()
}
@@ -833,20 +836,36 @@ impl OpenCADStudio {
if s.chars().all(|c| !c.is_control()) {
self.command_line.input.push_str(&s);
}
self.command_line.autocomplete_cursor = None;
self.focus_cmd_input()
}
Message::CommandBackspace => {
self.command_line.input.pop();
self.command_line.autocomplete_cursor = None;
self.focus_cmd_input()
}
Message::CommandHistoryPrev => {
// While autocomplete is showing suggestions, ↑ walks up
// that list. Otherwise it falls back to recall history.
let i = self.active_tab;
if self.tabs[i].active_cmd.is_none()
&& self.command_line.autocomplete_prev()
{
return Task::none();
}
self.command_line.history_prev();
Task::none()
}
Message::CommandHistoryNext => {
let i = self.active_tab;
if self.tabs[i].active_cmd.is_none()
&& self.command_line.autocomplete_next()
{
return Task::none();
}
self.command_line.history_next();
Task::none()
}
@@ -856,11 +875,29 @@ impl OpenCADStudio {
Task::none()
}
Message::CommandSuggestionPick(cmd) => {
self.command_line.input.clear();
self.command_line.close_history();
self.dispatch_command(&cmd)
}
Message::CommandSubmit => {
// Submitting a command implicitly dismisses the history
// dropdown so the dispatched command's new prompt is
// immediately visible on the overlay.
self.command_line.close_history();
// If the user navigated the autocomplete list with the
// arrow keys, Enter dispatches the highlighted command
// rather than the partial text actually in the buffer.
let i_tab = self.active_tab;
if self.tabs[i_tab].active_cmd.is_none() {
if let Some(picked) = self.command_line.selected_suggestion() {
let cmd = picked.to_string();
self.command_line.input.clear();
self.command_line.autocomplete_cursor = None;
return self.dispatch_command(&cmd);
}
}
let i = self.active_tab;
if self.tabs[i].active_cmd.is_some() {
let text = self.command_line.input.trim().to_string();
@@ -971,6 +1008,16 @@ impl OpenCADStudio {
{
return Task::none();
}
// Typed text on the command line cancels first — one
// Esc empties the buffer, a second Esc then escalates
// to whatever the current mode would otherwise do
// (cancel command / exit viewport / deselect).
if !self.command_line.input.is_empty() {
self.command_line.input.clear();
self.command_line.autocomplete_cursor = None;
self.command_line.close_history();
return Task::none();
}
let i = self.active_tab;
if self.tabs[i].active_cmd.is_some() {
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape());
+4 -1
View File
@@ -508,7 +508,10 @@ impl OpenCADStudio {
// viewport stack rather than as a separate row in the main
// column — frees up vertical space when no command is active
// and keeps the input close to where the cursor is drawing.
let command_line_overlay = iced::widget::container(self.command_line.view())
// Autocomplete shows only when no command is collecting its
// own input (otherwise typed prefixes are coordinates / values).
let allow_autocomplete = tab.active_cmd.is_none();
let command_line_overlay = iced::widget::container(self.command_line.view(allow_autocomplete))
.width(Fill)
.height(Fill)
.align_x(iced::alignment::Horizontal::Center)