Skip to main content

Mountain/Environment/
CommandProvider.rs

1//! # CommandProvider (Environment)
2//!
3//! Implements the `CommandExecutor` trait, serving as the central registry and
4//! dispatcher for all commands in the Mountain application. Commands can be
5//! handled either by native Rust handlers or proxied to extension sidecar
6//! processes.
7//!
8//! ## RESPONSIBILITIES
9//!
10//! ### 1. Command Registry
11//! - Maintain centralized registry of all registered commands
12//! - Store command metadata (id, title, category, flags)
13//! - Track command source (native vs extension)
14//! - Support command enablement/disable state
15//!
16//! ### 2. Command Dispatching
17//! - Route command execution requests to appropriate handlers
18//! - Execute native commands directly via function calls
19//! - Proxy extension commands via IPC to sidecar processes
20//! - Handle command result propagation and error translation
21//!
22//! ### 3. Command Execution Context
23//! - Provide `CommandExecutor` capability to command handlers
24//! - Manage command scope (text editor, file system, etc.)
25//! - Track command invocation source and user context
26//! - Support command cancellation for long-running operations
27//!
28//! ### 4. Command Discovery
29//! - Enumerate all registered commands for UI display
30//! - Support command palette and quick open
31//! - Provide command categories and visibility rules
32//! - Handle command contribution points from extensions
33//!
34//! ## ARCHITECTURAL ROLE
35//!
36//! CommandProvider is the **command execution hub** for Mountain:
37//!
38//! ```text
39//! Command Caller ──► CommandProvider ──► Handler (Native or Extension)
40//!       │                            │
41//!       └─► ExecuteCommand() ───────► Execute
42//! ```
43//!
44//! ### Position in Mountain
45//! - `Environment` module: Core capability provider
46//! - Implements `CommonLibrary::Command::CommandExecutor` trait
47//! - Accessible via `Environment.Require<dyn CommandExecutor>()`
48//!
49//! ### Command Sources
50//! - **Native Commands**: Defined in Rust, registered at startup
51//! - **Extension Commands**: Defined in package.json, contributed by extensions
52//! - **Built-in Commands**: Core Mountain functionality
53//! - **User-defined Commands**: Custom macros and keybindings (future)
54//!
55//! ### Dependencies
56//! - `ApplicationState`: Command registry storage
57//! - `IPCProvider`: For proxying to extension sidecars
58//! - `Log`: For command execution tracing
59//!
60//! ### Dependents
61//! - All command handlers: Use `CommandExecutor` to execute other commands
62//! - `DispatchLogic`::`DispatchFrontendCommand`: Main entry point
63//! - Tauri command handlers: Many invoke `ExecuteCommand`
64//! - Keybinding system: Trigger commands via keyboard shortcuts
65//!
66//! ## COMMAND EXECUTION FLOW
67//!
68//! 1. **Request**: Caller invokes `ExecuteCommand(command_id, args)`
69//! 2. **Lookup**: Provider looks up command in
70//!    `ApplicationState::CommandRegistry`
71//! 3. **Handler**: Retrieves the associated handler (native function or
72//!    extension RPC)
73//! 4. **Execute**: Calls handler with arguments
74//! 5. **Result**: Returns serialized JSON result or error
75//!
76//! ## NATIVE vs EXTENSION COMMANDS
77//!
78//! ### Native Commands
79//! - Implemented directly in Rust
80//! - Registered via `RegisterCommand` function
81//! - Handler is a function pointer or `Arc<Fn>`
82//! - Zero IPC overhead, direct call
83//!
84//! ### Extension Commands
85//! - Defined in extension's `package.json` `contributes.commands`
86//! - Registered when extension is activated
87//! - Handler is RPC method to extension sidecar
88//! - Goes through IPC layer with serialization
89//!
90//! ## ERROR HANDLING
91//!
92//! - Command not found: Returns `CommonError::InvalidArgument`
93//! - Handler errors: Propagated as `CommonError`
94//! - IPC failures: Converted to `CommonError::IPCError`
95//! - Serialization failures: `CommonError::SerializationError`
96//!
97//! ## PERFORMANCE
98//!
99//! - Native commands: Near-zero overhead (direct function call)
100//! - Extension commands: IPC serialization + network latency
101//! - Command lookup: HashMap lookup by string ID (fast)
102//! - Consider caching frequently used command results
103//!
104//! ## VS CODE REFERENCE
105//!
106//! Borrowed from VS Code's command system:
107//! - `vs/platform/commands/common/commands.ts` - Command definitions
108//! - `vs/workbench/services/commands/common/commandService.ts` - Command
109//!   registry
110//! - `vs/platform/commands/common/commandExecutor.ts` - Command execution
111//!
112//! ## TODO
113//!
114//! - [ ] Implement command contribution points from extensions
115//! - [ ] Add command enablement/disable state management
116//! - [ ] Support command categories and grouping
117//! - [ ] Add command history and undo/redo stack
118//! - [ ] Implement command keyboard shortcut resolution
119//! - [ ] Add command telemetry (usage metrics)
120//! - [ ] Support command aliases and deprecation
121//! - [ ] Add command permission validation
122//! - [ ] Implement command batching for related operations
123//!
124//! ## MODULE CONTENTS
125//!
126//! - `CommandProvider`: Main struct implementing `CommandExecutor`
127//! - Command registration functions (to be added)
128//! - Extension command proxy logic
129
130// 1. **Command Registry**: Maintains a centralized registry of all registered
131//    commands and their corresponding handlers (native or proxied).
132//
133// 2. **Command Dispatching**: Routes command execution requests to the
134//    appropriate handler based on the command identifier.
135//
136// 3. **Extension Command Proxying**: Enables extensions to contribute commands
137//    that are executed in their sidecar processes via IPC.
138//
139// 4. **Command Lifecycle Management**: Handles registration, unregistration,
140//    and querying of available commands.
141//
142// # Command Execution Flow
143//
144// 1. Extension or system calls ExecuteCommand(identifier, args)
145// 2. CommandProvider looks up the command in
146//    ApplicationState.Extension.Registry.CommandRegistry
147// 3. If native handler: executes Rust function directly with AppHandle and
148//    arguments
149// 4. If proxied handler: sends IPC request to the owning sidecar via Vine
150//    client
151// 5. Returns result or error to caller
152//
153// # Patterns Borrowed from VSCode
154//
155// - **Command Registry**: Follows VSCode's command registry pattern where
156//   commands are identified by strings and can be contributed by extensions.
157//
158// - **Context Passing**: Like VSCode's execution context, Mountain passes the
159//   AppHandle and Runtime to native handlers for context awareness.
160//
161// - **Conflict Resolution**: VSCode allows command overrides; Mountain
162//   currently does not implement conflict resolution (TODO).
163//
164// # TODOs
165//
166// - [ ] Implement command conflict resolution strategy
167// - [ ] Add command execution context (selection, active editor, etc.)
168// - [ ] Implement command categories and metadata
169// - [ ] Add command enablement/disablement based on context
170// - [ ] Implement command execution metrics and telemetry
171// - [ ] Add command keyboard shortcut registration lookup
172// - [ ] Implement command execution timeout and cancellation
173// - [ ] Add validation of command arguments
174// - [ ] Consider adding command preconditions
175// - [ ] Implement command history for undo/redo scenarios
176
177use std::{future::Future, pin::Pin, sync::Arc};
178
179use CommonLibrary::{
180	Command::CommandExecutor::CommandExecutor,
181	Error::CommonError::CommonError,
182	IPC::DTO::ProxyTarget::ProxyTarget,
183};
184use async_trait::async_trait;
185use serde_json::{Value, json};
186use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
187
188use super::MountainEnvironment::MountainEnvironment;
189use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, Vine::Client, dev_log};
190
191/// An enum representing the different ways a command can be handled.
192pub enum CommandHandler<R:Runtime + 'static> {
193	/// A command handled by a native, asynchronous Rust function.
194	Native(
195		fn(
196			AppHandle<R>,
197
198			WebviewWindow<R>,
199
200			Arc<ApplicationRunTime>,
201
202			Value,
203		) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>>,
204	),
205
206	/// A command implemented in an extension and proxied to a sidecar.
207	Proxied { SideCarIdentifier:String, CommandIdentifier:String },
208}
209
210impl<R:Runtime> Clone for CommandHandler<R> {
211	fn clone(&self) -> Self {
212		match self {
213			Self::Native(Function) => Self::Native(*Function),
214
215			Self::Proxied { SideCarIdentifier, CommandIdentifier } => {
216				Self::Proxied {
217					SideCarIdentifier:SideCarIdentifier.clone(),
218
219					CommandIdentifier:CommandIdentifier.clone(),
220				}
221			},
222		}
223	}
224}
225
226#[async_trait]
227impl CommandExecutor for MountainEnvironment {
228	/// Executes a registered command by dispatching it to the appropriate
229	/// handler.
230	async fn ExecuteCommand(&self, CommandIdentifier:String, Argument:Value) -> Result<Value, CommonError> {
231		let HandlerInfoOption = self
232			.ApplicationState
233			.Extension
234			.Registry
235			.CommandRegistry
236			.lock()
237			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
238			.get(&CommandIdentifier)
239			.cloned();
240
241		match HandlerInfoOption {
242			Some(CommandHandler::Native(Function)) => {
243				// Per-execution line. The setContext dominator is already
244				// gated in Command/Bootstrap.rs; other native commands
245				// (openWalkthrough, etc.) fire rarely enough that the
246				// surviving tag volume is low, but `commands-verbose`
247				// keeps this opt-in for consistency.
248				dev_log!(
249					"commands-verbose",
250					"[CommandProvider] Executing NATIVE command '{}'.",
251					CommandIdentifier
252				);
253
254				let RunTime:Arc<ApplicationRunTime> =
255					self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
256
257				let MainWindow = self.ApplicationHandle.get_webview_window("main").ok_or_else(|| {
258					CommonError::UserInterfaceInteraction {
259						Reason:"Main window not found for command execution".into(),
260					}
261				})?;
262
263				Function(self.ApplicationHandle.clone(), MainWindow, RunTime, Argument)
264					.await
265					.map_err(|Error| CommonError::CommandExecution { CommandIdentifier, Reason:Error })
266			},
267
268			Some(CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier: ProxiedCommandIdentifier }) => {
269				dev_log!(
270					"commands-verbose",
271					"[CommandProvider] Executing PROXIED command '{}' on sidecar '{}'.",
272					CommandIdentifier,
273					SideCarIdentifier
274				);
275
276				let RPCParameters = json!([ProxiedCommandIdentifier, Argument]);
277
278				let RPCMethod = format!("{}$ExecuteContributedCommand", ProxyTarget::ExtHostCommands.GetTargetPrefix());
279
280				Client::SendRequest::Fn(&SideCarIdentifier, RPCMethod, RPCParameters, 30000)
281					.await
282					.map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
283			},
284
285			None => {
286				// VS Code auto-registers `<viewId>.focus`,
287				// `<viewId>.resetViewLocation`, and `<viewId>.removeView`
288				// commands when a view is contributed via the view registry.
289				// Land's webview.registerView bypasses that registry and
290				// emits a Tauri event instead, so the focus commands never
291				// get inserted. Extensions (gitlens in particular) call
292				// `commands.executeCommand('<their-view-id>.focus')` on
293				// user gesture; the Cocoon try/catch swallows the error,
294				// but the red `error:` log noise here is misleading. Treat
295				// these well-known auto-generated suffixes as silent no-ops.
296				if CommandIdentifier.ends_with(".focus")
297					|| CommandIdentifier.ends_with(".resetViewLocation")
298					|| CommandIdentifier.ends_with(".removeView")
299				{
300					// Once-per-command-id so the no-op fallback doesn't
301					// generate an N-line trail through the dev log every
302					// time the user clicks a view-action button. The
303					// first occurrence still fires (documents the probe
304					// shape); subsequent invocations of the same command
305					// are silent.
306					crate::IPC::DevLog::DebugOnce(
307						"commands",
308						&format!("view-action-noop:{}", CommandIdentifier),
309						&format!(
310							"[CommandProvider] View-action command '{}' not registered; treating as no-op \
311							 (auto-generated by view registry in stock VS Code).",
312							CommandIdentifier
313						),
314					);
315					return Ok(Value::Null);
316				}
317
318				// Workbench-internal commands that stock VS Code registers on
319				// the renderer side via `CommandsRegistry.registerCommand(…)`
320				// but that Land doesn't carry because the backing service
321				// doesn't exist:
322				//
323				// - `getTelemetrySenderObject` - `vs/platform/telemetry/**` registers this so
324				//   extensions can fetch a `TelemetrySender` via `commands.executeCommand`.
325				//   Land has no telemetry backend, so returning null (no sender) matches the
326				//   "telemetry disabled" code path every extension already defensively handles.
327				// - `testing.clearTestResults` - registered by
328				//   `vs/workbench/contrib/testing/browser/testExplorerActions.ts`. No
329				//   test-explorer UI in Land today; null is the correct "nothing to clear"
330				//   shape.
331				//
332				// Extensions that look these up defensively try/catch. The
333				// only observable effect of the prior error return was the
334				// red `error:` log line. Treat as silent no-ops until Land
335				// grows the corresponding services.
336				if matches!(
337					CommandIdentifier.as_str(),
338					"getTelemetrySenderObject" | "testing.clearTestResults"
339				) {
340					// `getTelemetrySenderObject` fires once per extension
341					// activation (~30+ times per boot) - same once-per-id
342					// dedup as the view-action path so the log line
343					// documents the probe but doesn't trail.
344					crate::IPC::DevLog::DebugOnce(
345						"commands",
346						&format!("workbench-internal-noop:{}", CommandIdentifier),
347						&format!(
348							"[CommandProvider] Workbench-internal command '{}' not registered; treating as no-op \
349							 (Land has no backing service).",
350							CommandIdentifier
351						),
352					);
353					return Ok(Value::Null);
354				}
355
356				// TOCTOU race: Cocoon's `registerCommand` notification is
357				// fire-and-forget async, so Mountain's registry doesn't
358				// reflect a just-registered command for several ms. The
359				// TypeScript extension's post-activation pipeline invokes
360				// `_typescript.configurePlugin` within the same event-loop
361				// tick as its own `registerCommand`; the intervening
362				// executeCommand finds no handler and we emit an
363				// alarming red error: line.
364				//
365				// These internal-underscore-prefixed commands (the VS Code
366				// convention for "not-user-facing, extension-internal")
367				// are all bootstrap-phase hooks the extension expects to
368				// be safely droppable if the registry hasn't caught up yet.
369				// Return Value::Null - the extension's own try/catch
370				// takes the expected "not yet available" path. The next
371				// user gesture triggers a fresh call that finds the
372				// command registered normally.
373				if CommandIdentifier.starts_with("_typescript.")
374					|| CommandIdentifier.starts_with("_extensionHost.")
375					|| CommandIdentifier.starts_with("_workbench.registerWebview")
376					|| CommandIdentifier.ends_with(".activationCompleted")
377					|| CommandIdentifier.ends_with(".activated")
378					|| CommandIdentifier.ends_with(".ready")
379				{
380					dev_log!(
381						"commands",
382						"[CommandProvider] Activation-race command '{}' not yet in registry; returning null \
383						 (extension will retry post-activation).",
384						CommandIdentifier
385					);
386					return Ok(Value::Null);
387				}
388
389				// Lazy activation: stock VS Code fires
390				// `$activateByEvent("onCommand:<cmd>")` whenever a
391				// command-not-found lookup matches an extension's
392				// declared activation events. The extension then
393				// registers its command during activation, and the
394				// second registry lookup succeeds. Without this flow,
395				// any extension that gates on `onCommand:<id>` (e.g.
396				// GitLens' primary commands, Roo-Cline's commands, Vim
397				// mode toggles) never activates in response to a user
398				// gesture - it just silently does nothing.
399				if LookupCommandContributingExtension(self, &CommandIdentifier) {
400					dev_log!(
401						"commands",
402						"[CommandProvider] Lazy activation for command '{}' - firing onCommand:{0}",
403						CommandIdentifier
404					);
405					let Event = format!("onCommand:{}", CommandIdentifier);
406					let ActivationResult = Client::SendRequest::Fn(
407						&"cocoon-main".to_string(),
408						"$activateByEvent".to_string(),
409						json!({ "activationEvent": Event }),
410						30_000,
411					)
412					.await;
413					if let Err(Error) = ActivationResult {
414						dev_log!(
415							"commands",
416							"warn: [CommandProvider] onCommand:{} activation failed: {}",
417							CommandIdentifier,
418							Error
419						);
420					}
421					// Small yield so Cocoon's fire-and-forget
422					// `registerCommand` notification reaches Mountain's
423					// registry before the re-poll.
424					tokio::time::sleep(std::time::Duration::from_millis(50)).await;
425					let PostActivationHandler = self
426						.ApplicationState
427						.Extension
428						.Registry
429						.CommandRegistry
430						.lock()
431						.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
432						.get(&CommandIdentifier)
433						.cloned();
434					if let Some(Handler) = PostActivationHandler {
435						match Handler {
436							CommandHandler::Native(Function) => {
437								let MainWindow =
438									self.ApplicationHandle.get_webview_window("main").ok_or_else(|| {
439										CommonError::IPCError {
440											Description:"Could not find main window for lazy-activated native command"
441												.to_string(),
442										}
443									})?;
444								let RunTime =
445									self.ApplicationHandle.try_state::<Arc<ApplicationRunTime>>().ok_or_else(|| {
446										CommonError::IPCError {
447											Description:"ApplicationRunTime unavailable for lazy-activated native \
448											             command"
449												.to_string(),
450										}
451									})?;
452								return Function(
453									self.ApplicationHandle.clone(),
454									MainWindow,
455									(*RunTime).clone(),
456									Argument,
457								)
458								.await
459								.map_err(|Error| CommonError::CommandExecution { CommandIdentifier, Reason:Error });
460							},
461							CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier: ProxiedId } => {
462								let RPCParameters = json!([ProxiedId, Argument]);
463								let RPCMethod = format!(
464									"{}$ExecuteContributedCommand",
465									ProxyTarget::ExtHostCommands.GetTargetPrefix()
466								);
467								return Client::SendRequest::Fn(&SideCarIdentifier, RPCMethod, RPCParameters, 30_000)
468									.await
469									.map_err(|Error| CommonError::IPCError { Description:Error.to_string() });
470							},
471						}
472					}
473				}
474
475				dev_log!(
476					"commands",
477					"error: [CommandProvider] Command '{}' not found in registry.",
478					CommandIdentifier
479				);
480
481				Err(CommonError::CommandNotFound { Identifier:CommandIdentifier })
482			},
483		}
484	}
485
486	/// Registers a command contributed by a sidecar process.
487	async fn RegisterCommand(&self, SideCarIdentifier:String, CommandIdentifier:String) -> Result<(), CommonError> {
488		dev_log!(
489			"commands",
490			"[CommandProvider] Registering PROXY command '{}' from sidecar '{}'",
491			CommandIdentifier,
492			SideCarIdentifier
493		);
494
495		let mut Registry = self
496			.ApplicationState
497			.Extension
498			.Registry
499			.CommandRegistry
500			.lock()
501			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
502
503		Registry.insert(
504			CommandIdentifier.clone(),
505			CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier },
506		);
507
508		Ok(())
509	}
510
511	/// Unregisters a previously registered command.
512	async fn UnregisterCommand(&self, _SideCarIdentifier:String, CommandIdentifier:String) -> Result<(), CommonError> {
513		dev_log!("commands", "[CommandProvider] Unregistering command '{}'", CommandIdentifier);
514
515		self.ApplicationState
516			.Extension
517			.Registry
518			.CommandRegistry
519			.lock()
520			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
521			.remove(&CommandIdentifier);
522
523		Ok(())
524	}
525
526	/// Gets a list of all currently registered command IDs.
527	async fn GetAllCommands(&self) -> Result<Vec<String>, CommonError> {
528		dev_log!("commands", "[CommandProvider] Getting all command identifiers.");
529
530		let Registry = self
531			.ApplicationState
532			.Extension
533			.Registry
534			.CommandRegistry
535			.lock()
536			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
537
538		Ok(Registry.keys().cloned().collect())
539	}
540}
541
542/// Return `true` when some scanned extension declares
543/// `onCommand:<CommandIdentifier>` as one of its activation events. Used
544/// by the lazy-activation fallback in `ExecuteCommand` - without this
545/// check we'd fire an `$activateByEvent("onCommand:X")` for every
546/// unknown command, which would cause Cocoon to log "no extension
547/// matching event" for every typo. Scans the cached registry; no IPC.
548fn LookupCommandContributingExtension(Environment:&MountainEnvironment, CommandIdentifier:&str) -> bool {
549	let Event = format!("onCommand:{}", CommandIdentifier);
550	let Guard = match Environment
551		.ApplicationState
552		.Extension
553		.ScannedExtensions
554		.ScannedExtensions
555		.lock()
556	{
557		Ok(G) => G,
558		Err(_) => return false,
559	};
560	for Description in Guard.values() {
561		if let Some(Events) = &Description.ActivationEvents {
562			if Events.iter().any(|E| E == &Event) {
563				return true;
564			}
565		}
566	}
567	false
568}