Mountain/Environment/KeybindingProvider.rs
1//! # KeybindingProvider (Environment)
2//!
3//! Implements the `KeybindingProvider` trait for `MountainEnvironment`,
4//! providing keybinding resolution, conflict detection, and command activation
5//! based on keyboard input.
6//!
7//! ## RESPONSIBILITIES
8//!
9//! ### 1. Keybinding Collection
10//! - Gather default keybindings from all extensions (`contributes.keybindings`)
11//! - Load user-defined keybindings from `keybindings.json`
12//! - Include built-in Mountain keybindings
13//! - Support platform-specific keybinding variants
14//!
15//! ### 2. Keybinding Resolution
16//! - Parse keybinding combinations (modifiers + keys)
17//! - Evaluate "when" clauses for context activation
18//! - Resolve conflicts using priority rules:
19//! - User keybindings override extension keybindings
20//! - Extension keybindings override defaults
21//! - Within same priority, use specificity scoring
22//!
23//! ### 3. Command Activation
24//! - Match incoming keyboard events to keybindings
25//! - Execute bound commands with proper arguments
26//! - Handle keybinding chords (multi-key sequences)
27//! - Support keybinding cancellation (Esc, etc.)
28//!
29//! ### 4. Platform Adaptation
30//! - Convert between platform-specific modifiers (Cmd on macOS, Ctrl on
31//! Windows/Linux)
32//! - Support international keyboard layouts
33//! - Handle platform-exclusive keybindings
34//!
35//! ## ARCHITECTURAL ROLE
36//!
37//! KeybindingProvider is the **keyboard input mapper**:
38//!
39//! ```text
40//! Keyboard Event ──► KeybindingProvider ──► Resolved Keybinding ──► CommandExecutor
41//! ```
42//!
43//! ### Position in Mountain
44//! - `Environment` module: Input handling capability
45//! - Implements `CommonLibrary::Keybinding::KeybindingProvider` trait
46//! - Accessible via `Environment.Require<dyn KeybindingProvider>()`
47//!
48//! ### Keybinding Source Precedence (highest to lowest)
49//! 1. **User keybindings**: `keybindings.json` with overrides and unbindings
50//! 2. **Extension keybindings**: From `contributes.keybindings` in package.json
51//! 3. **Built-in keybindings**: Mountain default shortcuts
52//!
53//! ### Conflict Resolution
54//!
55//! When multiple keybindings match an input:
56//! 1. **Priority**: User > Extension > Built-in
57//! 2. **Specificity**: More specific "when" clause wins
58//! 3. **Scoring**: (TODO) Calculate score based on:
59//! - Number of modifiers (more is more specific)
60//! - Presence of "when" clause conditions
61//! - Platform specificity
62//!
63//! ## DATA MODEL
64//!
65//! A `Keybinding` consists of:
66//! - `Command`: Command ID to execute
67//! - `Keybinding`: Keyboard shortcut string (e.g., "Ctrl+Shift+P")
68//! - `When`: Context expression (e.g., "editorTextFocus")
69//! - `Source`: Where it came from (user, extension, built-in)
70//! - `Weight`: Priority weight for conflict resolution
71//!
72//! ## WHEN CLAUSE EVALUATION
73//!
74//! "When" clauses are boolean expressions over context keys:
75//! - `editorTextFocus`: Editor has focus
76//! - `editorHasSelection`: Text is selected
77//! - `resourceLangId == python`: File is Python
78//! - `!editorHasSelection`: Negation
79//! - `editorTextFocus && !editorHasSelection`: AND combination
80//!
81//! Context keys are set by providers based on UI state:
82//! - Focus state, language, file type, editor mode, etc.
83//!
84//! ## PERFORMANCE
85//!
86//! - Keybinding lookup should be fast (HashMap or trie-based)
87//! - "When" clause evaluation should be efficient (pre-parsed expressions)
88//! - Consider caching resolved keybindings per context
89//! - Platform-specific lookup avoids runtime conversion overhead
90//!
91//! ## VS CODE REFERENCE
92//!
93//! Borrowed from VS Code's keybinding system:
94//! - `vs/platform/keybinding/common/keybinding.ts` - Keybinding data model
95//! - `vs/platform/keybinding/common/keybindingResolver.ts` - Resolution logic
96//! - `vs/platform/keybinding/common/keybindingsRegistry.ts` - Registry
97//! management
98//! - `vs/platform/contextkey/common/contextkey.ts` - "When" clause evaluation
99//!
100//! ## TODO
101//!
102//! - [ ] Implement complete "when" clause expression parser and evaluator
103//! - [ ] Add keybinding precedence scoring algorithm
104//! - [ ] Support keybinding localization (different key labels per locale)
105//! - [ ] Implement platform-specific modifier conversion (Cmd/Ctrl/Alt)
106//! - [ ] Add conflict detection and warnings during registration
107//! - [ ] Implement keybinding chords (e.g., "Ctrl+K Ctrl+C")
108//! - [ ] Support custom keybinding schemes (vim, emacs, sublime)
109//! - [ ] Add keybinding validation and syntax error reporting
110//! - [ ] Implement keybinding telemetry for usage tracking
111//! - [ ] Support keybinding migration across versions
112//! - [ ] Add keybinding export/import functionality
113//! - [ ] Implement keybinding search and discovery UI
114//! - [ ] Support keybinding recording from actual key presses
115//! - [ ] Add keybinding conflict resolution UI
116//! - [ ] Implement keybinding per-profile (development, debugging, etc.)
117//!
118//! ## MODULE CONTENTS
119//!
120//! - [`KeybindingProvider`]: Main struct implementing the trait
121//! - Keybinding collection and registration
122//! - "When" clause parsing and evaluation
123//! - Conflict resolution logic
124//! - Platform-specific key mapping
125//! - Command activation from keyboard events
126
127//! - User keybindings override system defaults
128//! - Negative commands (starting with `-`) unbind keys
129//! - Higher priority values win in cases of ambiguity
130//! 4. Evaluate when clauses at runtime to filter active keybindings
131//!
132//! ## When Clause Evaluation
133//!
134//! When clauses are boolean expressions controlling when a keybinding
135//! is active. Examples:
136//! - `"editorTextFocus && !inQuickOpen"` - Only when editor has focus
137//! - `"debugState != 'inactive'"` - Only when debugging
138//!
139//! Current implementation stores when clauses but only partially
140//! evaluates them. Full expression evaluation is pending.
141
142use std::{collections::HashMap, sync::Arc};
143
144use CommonLibrary::{
145 Effect::ApplicationRunTime::ApplicationRunTime as _,
146 Error::CommonError::CommonError,
147 FileSystem::ReadFile::ReadFile,
148 Keybinding::KeybindingProvider::KeybindingProvider,
149};
150use async_trait::async_trait;
151use serde_json::{Value, json};
152use tauri::Manager;
153
154use super::{MountainEnvironment::MountainEnvironment, Utility};
155use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
156
157#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
158#[serde(rename_all = "camelCase")]
159struct KeybindingRule {
160 key:String,
161
162 command:String,
163
164 when:Option<String>,
165
166 args:Option<Value>,
167}
168
169#[async_trait]
170impl KeybindingProvider for MountainEnvironment {
171 async fn GetResolvedKeybinding(&self) -> Result<Value, CommonError> {
172 dev_log!("keybinding", "[KeybindingProvider] Resolving all keybindings...");
173
174 let mut ResolvedKeybindings:HashMap<String, KeybindingRule> = HashMap::new();
175
176 // 1. Collect default keybindings from extensions
177 let Extensions = self
178 .ApplicationState
179 .Extension
180 .ScannedExtensions
181 .ScannedExtensions
182 .lock()
183 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?
184 .clone();
185
186 for Extension in Extensions.values() {
187 if let Some(Contributes) = Extension.Contributes.as_ref().and_then(|c| c.get("keybindings")) {
188 if let Some(KeybindingsArray) = Contributes.as_array() {
189 for KeybindingValue in KeybindingsArray {
190 if let Ok(KeybindingRule) = serde_json::from_value::<KeybindingRule>(KeybindingValue.clone()) {
191 let UniqueKey =
192 format!("{}{}", KeybindingRule.key, KeybindingRule.when.as_deref().unwrap_or(""));
193
194 ResolvedKeybindings.insert(UniqueKey, KeybindingRule);
195 }
196 }
197 }
198 }
199 }
200
201 // 2. Load and apply user-defined keybindings from keybindings.json
202 let UserKeybindingsPath = self
203 .ApplicationHandle
204 .path()
205 .app_config_dir()
206 .map_err(|Error| {
207 CommonError::ConfigurationLoad { Description:format!("Cannot find app config dir: {}", Error) }
208 })?
209 .join("keybindings.json");
210
211 let RunTime = self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
212
213 if let Ok(Content) = RunTime.Run(ReadFile(UserKeybindingsPath)).await {
214 if let Ok(UserKeybindings) = serde_json::from_slice::<Vec<KeybindingRule>>(&Content) {
215 for UserKeybinding in UserKeybindings {
216 let UniqueKey = format!("{}{}", UserKeybinding.key, UserKeybinding.when.as_deref().unwrap_or(""));
217
218 if UserKeybinding.command.starts_with('-') {
219 // Unbind rule
220 ResolvedKeybindings.remove(&UniqueKey);
221 } else {
222 // Override rule
223 ResolvedKeybindings.insert(UniqueKey, UserKeybinding);
224 }
225 }
226 } else {
227 dev_log!(
228 "keybinding",
229 "warn: [KeybindingProvider] Failed to parse user keybindings.json. It may be malformed."
230 );
231 }
232 }
233
234 let FinalRules:Vec<KeybindingRule> = ResolvedKeybindings.into_values().collect();
235
236 Ok(json!(FinalRules))
237 }
238}