Skip to main content

oo_ide/
commands.rs

1pub mod args;
2pub mod builder;
3pub mod id;
4pub mod meta;
5
6pub use args::{ArgKind, ArgValue, CommandArg};
7pub use builder::command;
8pub use id::CommandId;
9pub use meta::CommandMeta;
10
11use std::collections::{HashMap, VecDeque};
12use std::time::Instant;
13
14use crate::app_state::AppState;
15use crate::operation::Operation;
16
17pub type CommandHandler =
18    Box<dyn Fn(&AppState, &HashMap<String, ArgValue>) -> Vec<Operation>>;
19
20pub struct Command {
21    pub meta: CommandMeta,
22    /// Read-only context — commands declare intent via operations, never mutate.
23    pub handler: CommandHandler,
24}
25
26const HISTORY_LIMIT: usize = 50;
27
28#[derive(Debug, Clone)]
29pub struct HistoryEntry {
30    pub id: CommandId,
31    pub args: HashMap<String, ArgValue>,
32    pub timestamp: Instant,
33    pub count: u32,
34}
35
36pub struct CommandRegistry {
37    commands: HashMap<CommandId, Command>,
38    history: VecDeque<HistoryEntry>,
39}
40
41impl CommandRegistry {
42    pub fn new() -> Self {
43        Self {
44            commands: HashMap::new(),
45            history: VecDeque::new(),
46        }
47    }
48
49    pub fn register(&mut self, command: Command) {
50        self.commands.insert(command.meta.id.clone(), command);
51    }
52
53    /// Like `resolve_shortcut` but only returns the command if it is active in
54    /// at least one of the given contexts (or has no context restriction).
55    pub fn select_command_in_context<'a>(
56        &self,
57        ids: &'a Vec<CommandId>,
58        active_contexts: &std::collections::HashSet<String>,
59    ) -> Option<&'a CommandId> {
60        log::debug!("Select commands in context {ids:?} {active_contexts:?}");
61        for id in ids {
62            if let Some(cmd) = self.commands.get(id) {
63                let active = cmd.meta.contexts.is_empty()
64                    || cmd
65                        .meta
66                        .contexts
67                        .iter()
68                        .any(|c| active_contexts.contains(c.as_ref()));
69                log::debug!("{id:?} {active:?}");
70                if active {
71                    return Some(id);
72                }
73            }
74        }
75        None
76    }
77
78    #[allow(dead_code)]
79    pub fn active_commands<'a>(
80        &'a self,
81        app: &'a AppState,
82    ) -> impl Iterator<Item = &'a Command> + 'a {
83        self.commands.values().filter(move |cmd| {
84            cmd.meta.contexts.is_empty()
85                || cmd
86                    .meta
87                    .contexts
88                    .iter()
89                    .any(|c| app.active_contexts.contains(c.as_ref()))
90        })
91    }
92
93    #[allow(dead_code)]
94    pub fn all_commands_sorted(&self) -> Vec<&Command> {
95        let mut cmds: Vec<&Command> = self.commands.values().collect();
96        cmds.sort_by(|a, b| {
97            a.meta
98                .id
99                .group
100                .cmp(&b.meta.id.group)
101                .then(a.meta.id.name.cmp(&b.meta.id.name))
102        });
103        cmds
104    }
105
106    pub fn user_commands_sorted(&self) -> Vec<&Command> {
107        let mut cmds: Vec<&Command> = self
108            .commands
109            .values()
110            .filter(|c| matches!(c.meta.visibility, meta::Visibility::UserVisible))
111            .collect();
112        cmds.sort_by(|a, b| {
113            a.meta
114                .id
115                .group
116                .cmp(&b.meta.id.group)
117                .then(a.meta.id.name.cmp(&b.meta.id.name))
118        });
119        cmds
120    }
121
122    pub fn history(&self) -> &VecDeque<HistoryEntry> {
123        &self.history
124    }
125
126    /// Execute a command: returns the operations it produced.
127    /// Unknown ID → returns empty, logs to stderr (no AppState mutation allowed here).
128    pub fn execute(
129        &mut self,
130        id: &CommandId,
131        args: HashMap<String, ArgValue>,
132        app: &AppState,
133    ) -> Vec<Operation> {
134        if let Some(cmd) = self.commands.get(id) {
135            
136            // Do not record history here — callers decide whether to record.
137            (cmd.handler)(app, &args)
138        } else {
139            log::warn!("Unknown command: {}", id);
140            vec![]
141        }
142    }
143
144    fn push_history(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
145        // Do not record history for non-user-visible commands.
146        if let Some(cmd) = self.commands.get(&id) {
147            if !matches!(cmd.meta.visibility, meta::Visibility::UserVisible) {
148                return;
149            }
150        } else {
151            // Unknown command; don't record.
152            return;
153        }
154
155        if let Some(front) = self.history.front_mut()
156            && front.id == id
157            && front.args == args
158        {
159            front.count += 1;
160            front.timestamp = Instant::now();
161            return;
162        }
163        self.history.push_front(HistoryEntry {
164            id,
165            args,
166            timestamp: Instant::now(),
167            count: 1,
168        });
169        if self.history.len() > HISTORY_LIMIT {
170            self.history.pop_back();
171        }
172    }
173
174    /// Record a history entry for commands explicitly invoked via the command picker.
175    /// This honors command visibility (hidden commands are not recorded).
176    pub fn record_palette_selection(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
177        self.push_history(id, args);
178    }
179}
180
181impl Default for CommandRegistry {
182    fn default() -> Self {
183        Self::new()
184    }
185}