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 log::{info, warn};
152use serde_json::{Value, json};
153use tauri::Manager;
154
155use super::{MountainEnvironment::MountainEnvironment, Utility};
156use crate::RunTime::ApplicationRunTime::ApplicationRunTime;
157
158#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
159#[serde(rename_all = "camelCase")]
160struct KeybindingRule {
161	key:String,
162
163	command:String,
164
165	when:Option<String>,
166
167	args:Option<Value>,
168}
169
170#[async_trait]
171impl KeybindingProvider for MountainEnvironment {
172	async fn GetResolvedKeybinding(&self) -> Result<Value, CommonError> {
173		info!("[KeybindingProvider] Resolving all keybindings...");
174
175		let mut ResolvedKeybindings:HashMap<String, KeybindingRule> = HashMap::new();
176
177		// 1. Collect default keybindings from extensions
178		let Extensions = self
179			.ApplicationState
180			.Extension
181			.ScannedExtensions
182			.ScannedExtensions
183			.lock()
184			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
185			.clone();
186
187		for Extension in Extensions.values() {
188			if let Some(Contributes) = Extension.Contributes.as_ref().and_then(|c| c.get("keybindings")) {
189				if let Some(KeybindingsArray) = Contributes.as_array() {
190					for KeybindingValue in KeybindingsArray {
191						if let Ok(KeybindingRule) = serde_json::from_value::<KeybindingRule>(KeybindingValue.clone()) {
192							let UniqueKey =
193								format!("{}{}", KeybindingRule.key, KeybindingRule.when.as_deref().unwrap_or(""));
194
195							ResolvedKeybindings.insert(UniqueKey, KeybindingRule);
196						}
197					}
198				}
199			}
200		}
201
202		// 2. Load and apply user-defined keybindings from keybindings.json
203		let UserKeybindingsPath = self
204			.ApplicationHandle
205			.path()
206			.app_config_dir()
207			.map_err(|Error| {
208				CommonError::ConfigurationLoad { Description:format!("Cannot find app config dir: {}", Error) }
209			})?
210			.join("keybindings.json");
211
212		let RunTime = self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
213
214		if let Ok(Content) = RunTime.Run(ReadFile(UserKeybindingsPath)).await {
215			if let Ok(UserKeybindings) = serde_json::from_slice::<Vec<KeybindingRule>>(&Content) {
216				for UserKeybinding in UserKeybindings {
217					let UniqueKey = format!("{}{}", UserKeybinding.key, UserKeybinding.when.as_deref().unwrap_or(""));
218
219					if UserKeybinding.command.starts_with('-') {
220						// Unbind rule
221						ResolvedKeybindings.remove(&UniqueKey);
222					} else {
223						// Override rule
224						ResolvedKeybindings.insert(UniqueKey, UserKeybinding);
225					}
226				}
227			} else {
228				warn!("[KeybindingProvider] Failed to parse user keybindings.json. It may be malformed.");
229			}
230		}
231
232		let FinalRules:Vec<KeybindingRule> = ResolvedKeybindings.into_values().collect();
233
234		Ok(json!(FinalRules))
235	}
236}