diff --git a/Cargo.lock b/Cargo.lock index 8bd2d5532..e613eeda7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "glam 0.33.0", "iced", "image", + "inventory", "open", "printpdf", "rayon", @@ -2311,6 +2312,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "is-docker" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index ca129bbc8..782a5a1b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } rayon = "1" ureq = { version = "3", default-features = false, features = ["rustls"] } +inventory = "0.3" [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } diff --git a/src/app/mod.rs b/src/app/mod.rs index d0edd07c6..7832cca19 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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), diff --git a/src/app/update.rs b/src/app/update.rs index 4f9b3913d..d3afe3e8c 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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()); diff --git a/src/app/view.rs b/src/app/view.rs index 2042d16ab..80f8f79ee 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -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) diff --git a/src/command/mod.rs b/src/command/mod.rs index 3c7f58678..fb69fcffe 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -317,3 +317,29 @@ pub trait CadCommand: Send { /// Default: no-op. fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {} } + +// ── Autocomplete registry ───────────────────────────────────────────────── +// +// Every `impl CadCommand for Foo` module submits the names it answers to +// at compile time via `inventory::submit!`. The command-line autocomplete +// then iterates the resulting collection at runtime — no central list to +// keep in sync. +// +// Non-interactive one-shot dispatch arms (NEW, OPEN, SAVE, …) live in +// `app/commands.rs` and don't have a `CadCommand` impl; they're absent +// from autocomplete by design. Add an explicit `inventory::submit!` next +// to their dispatch arm if you want them surfaced. + +pub struct CommandRegistration { + pub names: &'static [&'static str], +} + +inventory::collect!(CommandRegistration); + +/// All registered command names, including aliases. +pub fn all_registered_command_names() -> Vec<&'static str> { + inventory::iter:: + .into_iter() + .flat_map(|r| r.names.iter().copied()) + .collect() +} diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index bab894a4d..60071cb15 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -149,3 +149,7 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel { fill_tris: vec![], } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DAL", "DIMALIGNED"] }); // AlignedDimensionCommand diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index de13ecdd1..55b619c1b 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -173,3 +173,7 @@ fn angular_preview(vertex: Vec3, first: Vec3, second: Vec3, arc_pt: Vec3) -> Vec } points } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); // AngularDimensionCommand diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 9df07210d..4ea620e9b 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -112,3 +112,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { fill_tris: vec![], } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DDI", "DIMDIAMETER"] }); // DiameterDimensionCommand diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index bd033457a..8187428ac 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -177,3 +177,7 @@ impl CadCommand for DimBaselineCommand { fn v3(p: Vec3) -> Vector3 { Vector3::new(p.x as f64, p.y as f64, p.z as f64) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DBA", "DIMBASELINE"] }); // DimBaselineCommand diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 1a5187387..22c9a25c6 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -158,3 +158,7 @@ impl CadCommand for DimContinueCommand { fn v3(p: Vec3) -> Vector3 { Vector3::new(p.x as f64, p.y as f64, p.z as f64) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DCO", "DIMCONTINUE"] }); // DimContinueCommand diff --git a/src/modules/annotate/dimbreak.rs b/src/modules/annotate/dimbreak.rs index eac7995bd..5172bdbfc 100644 --- a/src/modules/annotate/dimbreak.rs +++ b/src/modules/annotate/dimbreak.rs @@ -97,3 +97,7 @@ impl CadCommand for DimBreakCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DBR", "DIMBREAK"] }); // DimBreakCommand diff --git a/src/modules/annotate/dimedit.rs b/src/modules/annotate/dimedit.rs index fc2cf4e44..f5eadab43 100644 --- a/src/modules/annotate/dimedit.rs +++ b/src/modules/annotate/dimedit.rs @@ -85,3 +85,7 @@ impl CadCommand for DimEditCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DED", "DIMEDIT"] }); // DimEditCommand diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index d36cc4079..5f9cce128 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -108,3 +108,7 @@ impl CadCommand for DimJogLineCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE", "DJL"] }); // DimJogLineCommand diff --git a/src/modules/annotate/dimspace.rs b/src/modules/annotate/dimspace.rs index ccc30c0c8..1e0a9224d 100644 --- a/src/modules/annotate/dimspace.rs +++ b/src/modules/annotate/dimspace.rs @@ -125,3 +125,7 @@ impl CadCommand for DimSpaceCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMSPACE", "DSPACE"] }); // DimSpaceCommand diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index d5fe0935d..029d86712 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -128,3 +128,7 @@ impl CadCommand for DimTeditCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] }); // DimTeditCommand diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 5a09fb87f..80ff8df7b 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -252,3 +252,7 @@ pub fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] { ), ] } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["LE", "LEADER"] }); // LeaderCommand diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index fe8fdafdd..18735a2cc 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -163,3 +163,7 @@ fn linear_text_pos(first: Vec3, second: Vec3, def: Vec3) -> Vec3 { let d2 = second + perp * offset; (d1 + d2) * 0.5 + perp * 0.15 } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMLINEAR"] }); // LinearDimensionCommand diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index ac46f2f04..3885f9443 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -199,3 +199,7 @@ fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] { ), ] } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["MLD", "MLEADER"] }); // MLeaderCommand diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index d2b660bdc..b6d70a707 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -476,3 +476,10 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { // Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {} + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["MLA", "MLEADERADD"] }); // MLeaderAddCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLAL", "MLEADERALIGN"] }); // MLeaderAlignCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLC", "MLEADERCOLLECT"] }); // MLeaderCollectCommand +inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERREMOVE", "MLR"] }); // MLeaderRemoveCommand diff --git a/src/modules/annotate/mtext.rs b/src/modules/annotate/mtext.rs index 9f9eeca89..3e3680827 100644 --- a/src/modules/annotate/mtext.rs +++ b/src/modules/annotate/mtext.rs @@ -91,3 +91,7 @@ impl CadCommand for MTextCommand { None } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["MT", "MTEXT"] }); // MTextCommand diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 385ee4def..5b03da5bc 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -86,3 +86,7 @@ impl CadCommand for OrdinateDimCommand { fn v3(p: Vec3) -> Vector3 { Vector3::new(p.x as f64, 0.0, p.z as f64) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMORDINATE", "DOR"] }); // OrdinateDimCommand diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 311bd0766..88586c026 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -128,3 +128,7 @@ pub fn make_linear_dim(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> EntityType { dim.base.actual_measurement = dim.measurement(); EntityType::Dimension(Dimension::Linear(dim)) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] }); // QdimCommand diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 5e565267f..8a0a270af 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -117,3 +117,7 @@ fn preview_wire(points: Vec) -> WireModel { fill_tris: vec![], } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIMRADIUS"] }); // RadiusDimensionCommand diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index f819baa6f..799ccb937 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -169,3 +169,7 @@ impl CadCommand for TableCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] }); // TableCommand diff --git a/src/modules/annotate/text.rs b/src/modules/annotate/text.rs index 04f3823bc..61e41d73a 100644 --- a/src/modules/annotate/text.rs +++ b/src/modules/annotate/text.rs @@ -86,3 +86,7 @@ impl CadCommand for TextCommand { None } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DT", "T", "TEXT"] }); // TextCommand diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 0f270bfd8..1328a6510 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -108,3 +108,7 @@ impl CadCommand for ToleranceCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["TOL", "TOLERANCE"] }); // ToleranceCommand diff --git a/src/modules/home/clipboard/paste.rs b/src/modules/home/clipboard/paste.rs index d12ebb240..04a297c45 100644 --- a/src/modules/home/clipboard/paste.rs +++ b/src/modules/home/clipboard/paste.rs @@ -58,3 +58,7 @@ impl CadCommand for PasteCommand { self.wires.iter().map(|w| w.translated(delta)).collect() } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["PASTECLIP", "PC"] }); // PasteCommand diff --git a/src/modules/home/draw/arc.rs b/src/modules/home/draw/arc.rs index f9212a70b..361db1924 100644 --- a/src/modules/home/draw/arc.rs +++ b/src/modules/home/draw/arc.rs @@ -1102,3 +1102,16 @@ impl CadCommand for ArcCSLCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_3P"] }); // Arc3PCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSL"] }); // ArcCSLCommand +inventory::submit!(crate::command::CommandRegistration { names: &["A", "ARC"] }); // ArcCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCA"] }); // ArcSCACommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCE"] }); // ArcSCECommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] }); // ArcSCLCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SEA"] }); // ArcSEACommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SED"] }); // ArcSEDCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SER"] }); // ArcSERCommand diff --git a/src/modules/home/draw/attdef.rs b/src/modules/home/draw/attdef.rs index c5088770a..a5074d8b3 100644 --- a/src/modules/home/draw/attdef.rs +++ b/src/modules/home/draw/attdef.rs @@ -184,3 +184,7 @@ impl CadCommand for AttdefCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["ATTDEF"] }); // AttdefCommand diff --git a/src/modules/home/draw/circle.rs b/src/modules/home/draw/circle.rs index efc7b54e1..408fe80ad 100644 --- a/src/modules/home/draw/circle.rs +++ b/src/modules/home/draw/circle.rs @@ -910,3 +910,12 @@ impl CadCommand for CircleTTTCommand { None } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] }); // Circle2PCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] }); // Circle3PCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }); // CircleCDCommand +inventory::submit!(crate::command::CommandRegistration { names: &["C", "CIRCLE"] }); // CircleCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); // CircleTTRCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); // CircleTTTCommand diff --git a/src/modules/home/draw/donut.rs b/src/modules/home/draw/donut.rs index 113c47545..35b0c7741 100644 --- a/src/modules/home/draw/donut.rs +++ b/src/modules/home/draw/donut.rs @@ -132,3 +132,7 @@ fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType { p.vertices = vec![v0, v1]; EntityType::LwPolyline(p) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DO", "DONUT"] }); // DonutCommand diff --git a/src/modules/home/draw/ellipse.rs b/src/modules/home/draw/ellipse.rs index 9b9648e13..64469278b 100644 --- a/src/modules/home/draw/ellipse.rs +++ b/src/modules/home/draw/ellipse.rs @@ -532,3 +532,9 @@ fn line_wire(from: Vec3, to: Vec3) -> WireModel { fill_tris: vec![], } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["ELLIPSE_ARC"] }); // EllipseArcCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ELLIPSE_AXIS"] }); // EllipseAxisCommand +inventory::submit!(crate::command::CommandRegistration { names: &["EL", "ELLIPSE"] }); // EllipseCommand diff --git a/src/modules/home/draw/hatch.rs b/src/modules/home/draw/hatch.rs index 46b815753..15bd290ef 100644 --- a/src/modules/home/draw/hatch.rs +++ b/src/modules/home/draw/hatch.rs @@ -423,3 +423,9 @@ impl CadCommand for BoundaryCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["BOUNDARY"] }); // BoundaryCommand +inventory::submit!(crate::command::CommandRegistration { names: &["GRADIENT"] }); // GradientCommand +inventory::submit!(crate::command::CommandRegistration { names: &["H", "HATCH"] }); // HatchCommand diff --git a/src/modules/home/draw/hatchedit.rs b/src/modules/home/draw/hatchedit.rs index 8b2071cb5..f4f2eb18a 100644 --- a/src/modules/home/draw/hatchedit.rs +++ b/src/modules/home/draw/hatchedit.rs @@ -161,3 +161,7 @@ impl CadCommand for HatcheditCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["HATCHEDIT", "HE"] }); // HatcheditCommand diff --git a/src/modules/home/draw/line.rs b/src/modules/home/draw/line.rs index dfc3e4be8..27391b48e 100644 --- a/src/modules/home/draw/line.rs +++ b/src/modules/home/draw/line.rs @@ -94,3 +94,7 @@ impl CadCommand for LineCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["L", "LINE"] }); // LineCommand diff --git a/src/modules/home/draw/mline.rs b/src/modules/home/draw/mline.rs index fc1985e68..2e95fbe87 100644 --- a/src/modules/home/draw/mline.rs +++ b/src/modules/home/draw/mline.rs @@ -157,3 +157,7 @@ fn build_mline(pts: &[Vec3], scale: f64, closed: bool, style_name: &str) -> Enti mline.style_name = style_name.to_string(); EntityType::MLine(mline) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["ML", "MLINE"] }); // MlineCommand diff --git a/src/modules/home/draw/point.rs b/src/modules/home/draw/point.rs index a870fee92..6ff164e95 100644 --- a/src/modules/home/draw/point.rs +++ b/src/modules/home/draw/point.rs @@ -55,3 +55,7 @@ impl CadCommand for PointCommand { None } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["PO", "POINT"] }); // PointCommand diff --git a/src/modules/home/draw/polyline.rs b/src/modules/home/draw/polyline.rs index a3699e21d..69e2a467b 100644 --- a/src/modules/home/draw/polyline.rs +++ b/src/modules/home/draw/polyline.rs @@ -321,3 +321,7 @@ impl CadCommand for PlineCommand { )) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["PL", "PLINE"] }); // PlineCommand diff --git a/src/modules/home/draw/ray.rs b/src/modules/home/draw/ray.rs index c44a5ae62..46330762a 100644 --- a/src/modules/home/draw/ray.rs +++ b/src/modules/home/draw/ray.rs @@ -173,3 +173,8 @@ impl CadCommand for XLineCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XL", "XLINE"] }); // XLineCommand diff --git a/src/modules/home/draw/revcloud.rs b/src/modules/home/draw/revcloud.rs index bcfe9a706..5862c6af3 100644 --- a/src/modules/home/draw/revcloud.rs +++ b/src/modules/home/draw/revcloud.rs @@ -133,3 +133,7 @@ fn make_revcloud(pts: &[Vec3], arc_len: f64) -> EntityType { p.vertices = vertices; EntityType::LwPolyline(p) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["REVCLOUD"] }); // RevCloudCommand diff --git a/src/modules/home/draw/shapes.rs b/src/modules/home/draw/shapes.rs index 4ef109c63..7d03b3a09 100644 --- a/src/modules/home/draw/shapes.rs +++ b/src/modules/home/draw/shapes.rs @@ -633,3 +633,12 @@ fn edge_poly_params(a: Vec3, b: Vec3, sides: u32) -> Option<(Vec3, f32, f32)> { let sa = angle_xy(center, a); Some((center, vr, sa)) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["POLY_C"] }); // PolyCCommand +inventory::submit!(crate::command::CommandRegistration { names: &["POL", "POLY", "POLYGON"] }); // PolyCommand +inventory::submit!(crate::command::CommandRegistration { names: &["POLY_E"] }); // PolyECommand +inventory::submit!(crate::command::CommandRegistration { names: &["RECT_CEN"] }); // RectCenCommand +inventory::submit!(crate::command::CommandRegistration { names: &["REC", "RECT", "RECTANG"] }); // RectCommand +inventory::submit!(crate::command::CommandRegistration { names: &["RECT_ROT"] }); // RectRotCommand diff --git a/src/modules/home/draw/spline.rs b/src/modules/home/draw/spline.rs index e74ec85bc..35e1d8459 100644 --- a/src/modules/home/draw/spline.rs +++ b/src/modules/home/draw/spline.rs @@ -118,3 +118,7 @@ impl CadCommand for SplineCommand { )) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["SPL", "SPLINE"] }); // SplineCommand diff --git a/src/modules/home/groups/ungroup.rs b/src/modules/home/groups/ungroup.rs index 774c0d757..d38d9666e 100644 --- a/src/modules/home/groups/ungroup.rs +++ b/src/modules/home/groups/ungroup.rs @@ -59,3 +59,7 @@ impl CadCommand for UngroupCommand { vec![] } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["UG", "UNGROUP"] }); // UngroupCommand diff --git a/src/modules/home/inquiry/area.rs b/src/modules/home/inquiry/area.rs index b9c36db60..fbeb227fc 100644 --- a/src/modules/home/inquiry/area.rs +++ b/src/modules/home/inquiry/area.rs @@ -82,3 +82,7 @@ impl CadCommand for AreaCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] }); // AreaCommand diff --git a/src/modules/home/inquiry/dist.rs b/src/modules/home/inquiry/dist.rs index 83b410e71..4c179f2ad 100644 --- a/src/modules/home/inquiry/dist.rs +++ b/src/modules/home/inquiry/dist.rs @@ -77,3 +77,7 @@ impl CadCommand for DistCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DI", "DIST"] }); // DistCommand diff --git a/src/modules/home/inquiry/divide.rs b/src/modules/home/inquiry/divide.rs index ffe4baf7a..ee329b3db 100644 --- a/src/modules/home/inquiry/divide.rs +++ b/src/modules/home/inquiry/divide.rs @@ -295,3 +295,8 @@ fn arc_span_rad(start: f64, end: f64) -> f64 { span } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["DIV", "DIVIDE"] }); // DivideCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ME", "MEASURE"] }); // MeasureCommand diff --git a/src/modules/home/inquiry/id.rs b/src/modules/home/inquiry/id.rs index 6a5478558..e8e8540d5 100644 --- a/src/modules/home/inquiry/id.rs +++ b/src/modules/home/inquiry/id.rs @@ -34,3 +34,7 @@ impl CadCommand for IdCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["ID"] }); // IdCommand diff --git a/src/modules/home/layers/match_layer.rs b/src/modules/home/layers/match_layer.rs index 7290b05c5..f60dd2768 100644 --- a/src/modules/home/layers/match_layer.rs +++ b/src/modules/home/layers/match_layer.rs @@ -79,3 +79,7 @@ impl CadCommand for LayMatchCommand { vec![] } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["LAYMATCH", "LAYMCH"] }); // LayMatchCommand diff --git a/src/modules/home/modify/align.rs b/src/modules/home/modify/align.rs index 40f919cc6..0947a7460 100644 --- a/src/modules/home/modify/align.rs +++ b/src/modules/home/modify/align.rs @@ -189,3 +189,7 @@ impl AlignCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["AL", "ALIGN"] }); // AlignCommand diff --git a/src/modules/home/modify/break_cmd.rs b/src/modules/home/modify/break_cmd.rs index 040c0e2ee..6e11fb13a 100644 --- a/src/modules/home/modify/break_cmd.rs +++ b/src/modules/home/modify/break_cmd.rs @@ -467,3 +467,8 @@ impl CadCommand for BreakAtPointCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["BAP", "BREAKATPOINT"] }); // BreakAtPointCommand +inventory::submit!(crate::command::CommandRegistration { names: &["BR", "BREAK"] }); // BreakInteractiveCommand diff --git a/src/modules/home/modify/fillet.rs b/src/modules/home/modify/fillet.rs index dce445e74..729899f07 100644 --- a/src/modules/home/modify/fillet.rs +++ b/src/modules/home/modify/fillet.rs @@ -1706,3 +1706,8 @@ impl CadCommand for ChamferCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["CHA", "CHAMFER"] }); // ChamferCommand +inventory::submit!(crate::command::CommandRegistration { names: &["F", "FILLET"] }); // FilletCommand diff --git a/src/modules/home/modify/join.rs b/src/modules/home/modify/join.rs index 5094ff738..ae879570b 100644 --- a/src/modules/home/modify/join.rs +++ b/src/modules/home/modify/join.rs @@ -237,3 +237,7 @@ fn try_join_arcs(arcs: &[&(Handle, &EntityType)]) -> Option<(Vec, Vec Vector3 { Vector3::new(v.x as f64, v.y as f64, z) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["J", "JOIN"] }); // JoinCommand diff --git a/src/modules/home/modify/lengthen.rs b/src/modules/home/modify/lengthen.rs index 6c1074470..e530e5878 100644 --- a/src/modules/home/modify/lengthen.rs +++ b/src/modules/home/modify/lengthen.rs @@ -500,3 +500,7 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option bool { }, } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["PE", "PEDIT"] }); // PeditCommand diff --git a/src/modules/home/modify/refedit.rs b/src/modules/home/modify/refedit.rs index a1c6ebcf5..a77ddbbd4 100644 --- a/src/modules/home/modify/refedit.rs +++ b/src/modules/home/modify/refedit.rs @@ -206,3 +206,8 @@ pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEdit ); } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["REFCLOSE"] }); // RefCloseCommand +inventory::submit!(crate::command::CommandRegistration { names: &["REFEDIT"] }); // RefEditPickCommand diff --git a/src/modules/home/modify/splinedit.rs b/src/modules/home/modify/splinedit.rs index 701e15ca7..4fa3fcd67 100644 --- a/src/modules/home/modify/splinedit.rs +++ b/src/modules/home/modify/splinedit.rs @@ -200,3 +200,7 @@ pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle _ => {} } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["SPE", "SPLINEDIT"] }); // SplineditCommand diff --git a/src/modules/home/modify/trim.rs b/src/modules/home/modify/trim.rs index f4712bd34..45d627732 100644 --- a/src/modules/home/modify/trim.rs +++ b/src/modules/home/modify/trim.rs @@ -2457,3 +2457,8 @@ impl CadCommand for ExtendCommand { CmdResult::Cancel } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["EX", "EXTEND"] }); // ExtendCommand +inventory::submit!(crate::command::CommandRegistration { names: &["TR", "TRIM"] }); // TrimCommand diff --git a/src/modules/home/properties/match_prop.rs b/src/modules/home/properties/match_prop.rs index 51c12e9db..2f92c1bb1 100644 --- a/src/modules/home/properties/match_prop.rs +++ b/src/modules/home/properties/match_prop.rs @@ -89,3 +89,7 @@ impl CadCommand for MatchPropCommand { vec![] } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["MA", "MATCHPROP"] }); // MatchPropCommand diff --git a/src/modules/home/select.rs b/src/modules/home/select.rs index 49ccea18e..e1cbe7a49 100644 --- a/src/modules/home/select.rs +++ b/src/modules/home/select.rs @@ -60,3 +60,7 @@ impl CadCommand for SelectObjectsCommand { vec![] } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["AR", "ARRAY", "ARRAYPATH", "ARRAYPOLAR", "ARRAYRECT", "BLOCK", "CC", "CO", "COPY", "COPYCLIP", "CUTCLIP", "CX", "E", "ERASE", "EXPLODE", "G", "GROUP", "LAYFRZ", "LAYLCK", "LAYMCUR", "LAYOFF", "LAYULK", "M", "MI", "MIRROR", "MOVE", "RO", "ROTATE", "SC", "SCALE", "SS", "STRETCH", "X"] }); // SelectObjectsCommand diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index ea72e4611..655e10f51 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -210,3 +210,7 @@ impl InsertBlockCommand { } } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["INSERT"] }); // InsertBlockCommand diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 1ad96ed62..2b63601ac 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -658,3 +658,13 @@ impl CadCommand for LoftCommand { pub fn empty_solid3d() -> EntityType { EntityType::Solid3D(Solid3D::new()) } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["BOX"] }); // BoxCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CYLINDER"] }); // CylinderCommand +inventory::submit!(crate::command::CommandRegistration { names: &["EXT", "EXTRUDE"] }); // ExtrudeCommand +inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand +inventory::submit!(crate::command::CommandRegistration { names: &["REV", "REVOLVE"] }); // RevolveCommand +inventory::submit!(crate::command::CommandRegistration { names: &["SPHERE"] }); // SphereCommand +inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 4d586b764..5fcd8edde 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -104,3 +104,7 @@ impl CadCommand for MviewCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["MV", "MVIEW"] }); // MviewCommand diff --git a/src/modules/layout/vplayer.rs b/src/modules/layout/vplayer.rs index 2ac6c6105..45e87ef51 100644 --- a/src/modules/layout/vplayer.rs +++ b/src/modules/layout/vplayer.rs @@ -106,3 +106,7 @@ impl CadCommand for VplayerCommand { None } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["VPLAYER"] }); // VplayerCommand diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 7002f8083..dd9f4959e 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -75,3 +75,7 @@ impl CadCommand for PlotWindowCommand { }) } } + + +// ── Autocomplete registry ───────────────────────────────── +inventory::submit!(crate::command::CommandRegistration { names: &["PLOTWINDOW", "PW"] }); // PlotWindowCommand diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 255611b67..d479436d4 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -12,6 +12,9 @@ pub const CMD_INPUT_ID: &str = "cmd_input"; /// out. Picking the full archive happens through the dropdown button. const HISTORY_VISIBLE_SECS: f32 = 3.0; +/// How many autocomplete matches the suggestion popup shows at once. +const AUTOCOMPLETE_LIMIT: usize = 8; + fn cmd_input_id() -> iced::widget::Id { iced::widget::Id::new(CMD_INPUT_ID) } @@ -30,6 +33,10 @@ pub struct CommandLine { recall_draft: String, /// When `true`, the dropdown showing the full backlog is open. pub history_open: bool, + /// Index of the currently-highlighted autocomplete suggestion, or + /// `None` when the user hasn't yet started navigating with the + /// arrow keys. Reset on every keystroke. + pub autocomplete_cursor: Option, } #[derive(Clone, Debug)] @@ -152,7 +159,74 @@ impl CommandLine { self.history_open = false; } - pub fn view(&self) -> Element<'_, Message> { + /// Move the autocomplete highlight up one entry. Wraps to the last + /// match. Returns `true` when there was a list to navigate. + pub fn autocomplete_prev(&mut self) -> bool { + let len = self.autocomplete_matches().len(); + if len == 0 { + return false; + } + let next = match self.autocomplete_cursor { + None => len - 1, + Some(0) => len - 1, + Some(i) => i - 1, + }; + self.autocomplete_cursor = Some(next); + true + } + + /// Move the autocomplete highlight down one entry. Wraps to the + /// first match. Returns `true` when there was a list to navigate. + pub fn autocomplete_next(&mut self) -> bool { + let len = self.autocomplete_matches().len(); + if len == 0 { + return false; + } + let next = match self.autocomplete_cursor { + None => 0, + Some(i) if i + 1 < len => i + 1, + Some(_) => 0, + }; + self.autocomplete_cursor = Some(next); + true + } + + /// The command name the user has currently highlighted in the + /// autocomplete popup, if any. + pub fn selected_suggestion(&self) -> Option<&'static str> { + let matches = self.autocomplete_matches(); + self.autocomplete_cursor + .and_then(|i| matches.get(i).copied()) + } + + /// Autocomplete suggestions for the current input, capped at + /// [`AUTOCOMPLETE_LIMIT`]. Match is substring-anywhere (typing + /// `LEADER` surfaces `LEADER` and `MLEADER` and `QLEADER`); the + /// list is sorted so prefix matches come first. + /// + /// Names come from `crate::command::all_registered_command_names()` + /// which collects every `inventory::submit!` block placed next to a + /// `CadCommand` impl — no central list to maintain. + pub fn autocomplete_matches(&self) -> Vec<&'static str> { + let typed = self.input.trim(); + if typed.is_empty() { + return Vec::new(); + } + let needle = typed.to_uppercase(); + let mut matches: Vec<&'static str> = crate::command::all_registered_command_names() + .into_iter() + .filter(|cmd| cmd.contains(&needle)) + .collect(); + matches.sort(); + matches.dedup(); + // Prefix matches rank above mid-string ones, then alphabetical + // so the order is stable as the user keeps typing. + matches.sort_by_key(|cmd| (!cmd.starts_with(&needle), *cmd)); + matches.truncate(AUTOCOMPLETE_LIMIT); + matches + } + + pub fn view(&self, show_autocomplete: bool) -> Element<'_, Message> { // Only the most recent entries pushed within the last few // seconds show on the overlay. The dropdown button keeps the // full backlog reachable when the user actually wants it. @@ -207,6 +281,67 @@ impl CommandLine { }) .size(11) .padding([4, 6]); + // Autocomplete suggestions panel, shown above the input row + // when the user has typed a prefix that matches at least one + // command. Each row is a button — clicking it dispatches the + // command directly. + let autocomplete: Element<'_, Message> = if show_autocomplete { + let matches = self.autocomplete_matches(); + if matches.is_empty() { + container(column![]).height(0).into() + } else { + let cursor = self.autocomplete_cursor; + let mut col = column![].spacing(0).width(Length::Fill); + for (idx, cmd) in matches.iter().enumerate() { + let is_selected = cursor == Some(idx); + let row = button(text(*cmd).size(11).color(CMD_COLOR)) + .on_press(Message::CommandSuggestionPick(cmd.to_string())) + .width(Length::Fill) + .padding([2, 8]) + .style(move |_: &Theme, status| { + let bg = if is_selected { + Color { + r: 0.28, + g: 0.40, + b: 0.56, + a: 1.0, + } + } else if matches!(status, button::Status::Hovered) { + Color { + r: 0.22, + g: 0.30, + b: 0.42, + a: 1.0, + } + } else { + PANEL_BG + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::WHITE, + border: Border::default(), + ..Default::default() + } + }); + col = col.push(row); + } + container(col) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: BORDER_COLOR, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .width(Length::Fill) + .into() + } + } else { + container(column![]).height(0).into() + }; + // Dropdown trigger next to the input. Clicking it pops up the // full backlog (everything pushed since the app started) so the // user can recover anything that has already faded off the @@ -275,6 +410,7 @@ impl CommandLine { }; container(column![ + autocomplete, dropdown, container(history_rows) .style(|_: &Theme| container::Style { @@ -369,3 +505,4 @@ const INFO_COLOR: Color = Color { b: 0.90, a: 1.0, }; +