Skip to main content

Mountain/Environment/
CommandProvider.rs

1//! # CommandProvider
2//!
3//! Implements `CommandExecutor` for `MountainEnvironment` - the central
4//! registry and dispatcher for all commands in Mountain. Commands are
5//! identified by string IDs and handled either by native Rust functions
6//! or proxied to extension sidecar processes via IPC.
7//!
8//! ## Execution flow
9//!
10//! 1. Caller invokes `ExecuteCommand(id, args)`.
11//! 2. Provider looks up `id` in `ApplicationState::CommandRegistry`.
12//! 3. **Native handler**: calls the Rust function pointer directly with
13//!    `AppHandle`, the main `WebviewWindow`, `ApplicationRunTime`, and `args`.
14//! 4. **Proxied handler**: sends an RPC request to the owning extension sidecar
15//!    via the Vine IPC client.
16//! 5. Returns a serialized `serde_json::Value` result or a `CommonError`.
17//!
18//! ## Special-case no-ops
19//!
20//! Three categories of unknown commands are silently returned as
21//! `Value::Null` rather than an error, to match VS Code's behaviour:
22//! - View-action auto-commands (`.focus`, `.resetViewLocation`, `.removeView`)
23//! - Workbench-internal commands with no Land backing service
24//!   (`getTelemetrySenderObject`, `testing.clearTestResults`)
25//! - Bootstrap-phase activation-race commands (`_typescript.*`, etc.)
26//!
27//! ## Lazy activation
28//!
29//! If a command is not yet in the registry but a scanned extension declares
30//! `onCommand:<id>` as an activation event, `ExecuteCommand` fires
31//! `$activateByEvent` to Cocoon, yields 50 ms for the fire-and-forget
32//! `registerCommand` notification to arrive, then retries the lookup.
33//!
34//! ## Backlog
35//!
36//! - Contribution points from extensions; enablement/disable state
37//! - Categories, grouping, aliases, deprecation
38//! - History and undo/redo stack; keyboard shortcut resolution
39//! - Permission validation; batching for related operations; telemetry
40//!
41//! VS Code reference: `vs/platform/commands/common/commands.ts`,
42//! `vs/workbench/services/commands/common/commandService.ts`.
43
44use std::{future::Future, pin::Pin, sync::Arc};
45
46use CommonLibrary::{
47	Command::CommandExecutor::CommandExecutor,
48	Error::CommonError::CommonError,
49	IPC::DTO::ProxyTarget::ProxyTarget,
50};
51use async_trait::async_trait;
52use serde_json::{Value, json};
53use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
54use ::Vine::Client;
55
56use super::MountainEnvironment::MountainEnvironment;
57use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
58
59/// An enum representing the different ways a command can be handled.
60///
61/// Commands are either implemented as native Rust functions or
62/// delegated to extension sidecar processes via RPC.
63pub enum CommandHandler<R:Runtime + 'static> {
64	/// A command handled by a native, asynchronous Rust function.
65	Native(
66		fn(
67			AppHandle<R>,
68
69			WebviewWindow<R>,
70
71			Arc<ApplicationRunTime>,
72
73			Value,
74		) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>>,
75	),
76
77	/// A command implemented in an extension and proxied to a sidecar.
78	Proxied { SideCarIdentifier:String, CommandIdentifier:String },
79}
80
81impl<R:Runtime> Clone for CommandHandler<R> {
82	fn clone(&self) -> Self {
83		match self {
84			Self::Native(Function) => Self::Native(*Function),
85
86			Self::Proxied { SideCarIdentifier, CommandIdentifier } => {
87				Self::Proxied {
88					SideCarIdentifier:SideCarIdentifier.clone(),
89
90					CommandIdentifier:CommandIdentifier.clone(),
91				}
92			},
93		}
94	}
95}
96
97#[async_trait]
98impl CommandExecutor for MountainEnvironment {
99	/// Executes a registered command by dispatching it to the appropriate
100	/// handler.
101	async fn ExecuteCommand(&self, CommandIdentifier:String, Argument:Value) -> Result<Value, CommonError> {
102		let HandlerInfoOption = self
103			.ApplicationState
104			.Extension
105			.Registry
106			.CommandRegistry
107			.lock()
108			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
109			.get(&CommandIdentifier)
110			.cloned();
111
112		match HandlerInfoOption {
113			Some(CommandHandler::Native(Function)) => {
114				// Per-execution line. The setContext dominator is already
115				// gated in Command/Bootstrap.rs; other native commands
116				// (openWalkthrough, etc.) fire rarely enough that the
117				// surviving tag volume is low, but `commands-verbose`
118				// keeps this opt-in for consistency.
119				dev_log!(
120					"commands-verbose",
121					"[CommandProvider] Executing NATIVE command '{}'.",
122					CommandIdentifier
123				);
124
125				let RunTime:Arc<ApplicationRunTime> =
126					self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
127
128				let MainWindow = self.ApplicationHandle.get_webview_window("main").ok_or_else(|| {
129					CommonError::UserInterfaceInteraction {
130						Reason:"Main window not found for command execution".into(),
131					}
132				})?;
133
134				Function(self.ApplicationHandle.clone(), MainWindow, RunTime, Argument)
135					.await
136					.map_err(|Error| CommonError::CommandExecution { CommandIdentifier, Reason:Error })
137			},
138
139			Some(CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier: ProxiedCommandIdentifier }) => {
140				dev_log!(
141					"commands-verbose",
142					"[CommandProvider] Executing PROXIED command '{}' on sidecar '{}'.",
143					CommandIdentifier,
144					SideCarIdentifier
145				);
146
147				let RPCParameters = json!([ProxiedCommandIdentifier, Argument]);
148
149				let RPCMethod = format!("{}$ExecuteContributedCommand", ProxyTarget::ExtHostCommands.GetTargetPrefix());
150
151				Client::SendRequest::Fn(&SideCarIdentifier, RPCMethod, RPCParameters, 30000)
152					.await
153					.map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
154			},
155
156			None => {
157				// VS Code auto-registers `<viewId>.focus`,
158				// `<viewId>.resetViewLocation`, and `<viewId>.removeView`
159				// commands when a view is contributed via the view registry.
160				// Land's webview.registerView bypasses that registry and
161				// emits a Tauri event instead, so the focus commands never
162				// get inserted. Extensions (gitlens in particular) call
163				// `commands.executeCommand('<their-view-id>.focus')` on
164				// user gesture; the Cocoon try/catch swallows the error,
165				// but the red `error:` log noise here is misleading. Treat
166				// these well-known auto-generated suffixes as silent no-ops.
167				if CommandIdentifier.ends_with(".focus")
168					|| CommandIdentifier.ends_with(".resetViewLocation")
169					|| CommandIdentifier.ends_with(".removeView")
170				{
171					// Once-per-command-id so the no-op fallback doesn't
172					// generate an N-line trail through the dev log every
173					// time the user clicks a view-action button. The
174					// first occurrence still fires (documents the probe
175					// shape); subsequent invocations of the same command
176					// are silent.
177					crate::IPC::DevLog::DebugOnce::Fn(
178						"commands",
179						&format!("view-action-noop:{}", CommandIdentifier),
180						&format!(
181							"[CommandProvider] View-action command '{}' not registered; treating as no-op \
182							 (auto-generated by view registry in stock VS Code).",
183							CommandIdentifier
184						),
185					);
186
187					return Ok(Value::Null);
188				}
189
190				// Workbench-internal commands that stock VS Code registers on
191				// the renderer side via `CommandsRegistry.registerCommand(…)`
192				// but that Land doesn't carry because the backing service
193				// doesn't exist:
194				//
195				// - `getTelemetrySenderObject` - `vs/platform/telemetry/**` registers this so
196				//   extensions can fetch a `TelemetrySender` via `commands.executeCommand`.
197				//   Land has no telemetry backend, so returning null (no sender) matches the
198				//   "telemetry disabled" code path every extension already defensively handles.
199				// - `testing.clearTestResults` - registered by
200				//   `vs/workbench/contrib/testing/browser/testExplorerActions.ts`. No
201				//   test-explorer UI in Land today; null is the correct "nothing to clear"
202				//   shape.
203				//
204				// Extensions that look these up defensively try/catch. The
205				// only observable effect of the prior error return was the
206				// red `error:` log line. Treat as silent no-ops until Land
207				// grows the corresponding services.
208				if matches!(
209					CommandIdentifier.as_str(),
210					"getTelemetrySenderObject" | "testing.clearTestResults"
211				) {
212					// `getTelemetrySenderObject` fires once per extension
213					// activation (~30+ times per boot) - same once-per-id
214					// dedup as the view-action path so the log line
215					// documents the probe but doesn't trail.
216					crate::IPC::DevLog::DebugOnce::Fn(
217						"commands",
218						&format!("workbench-internal-noop:{}", CommandIdentifier),
219						&format!(
220							"[CommandProvider] Workbench-internal command '{}' not registered; treating as no-op \
221							 (Land has no backing service).",
222							CommandIdentifier
223						),
224					);
225
226					return Ok(Value::Null);
227				}
228
229				// TOCTOU race: Cocoon's `registerCommand` notification is
230				// fire-and-forget async, so Mountain's registry doesn't
231				// reflect a just-registered command for several ms. The
232				// TypeScript extension's post-activation pipeline invokes
233				// `_typescript.configurePlugin` within the same event-loop
234				// tick as its own `registerCommand`; the intervening
235				// executeCommand finds no handler and we emit an
236				// alarming red error: line.
237				//
238				// These internal-underscore-prefixed commands (the VS Code
239				// convention for "not-user-facing, extension-internal")
240				// are all bootstrap-phase hooks the extension expects to
241				// be safely droppable if the registry hasn't caught up yet.
242				// Return Value::Null - the extension's own try/catch
243				// takes the expected "not yet available" path. The next
244				// user gesture triggers a fresh call that finds the
245				// command registered normally.
246				if CommandIdentifier.starts_with("_typescript.")
247					|| CommandIdentifier.starts_with("_extensionHost.")
248					|| CommandIdentifier.starts_with("_workbench.registerWebview")
249					|| CommandIdentifier.ends_with(".activationCompleted")
250					|| CommandIdentifier.ends_with(".activated")
251					|| CommandIdentifier.ends_with(".ready")
252				{
253					dev_log!(
254						"commands",
255						"[CommandProvider] Activation-race command '{}' not yet in registry; returning null \
256						 (extension will retry post-activation).",
257						CommandIdentifier
258					);
259
260					return Ok(Value::Null);
261				}
262
263				// Lazy activation: stock VS Code fires
264				// `$activateByEvent("onCommand:<cmd>")` whenever a
265				// command-not-found lookup matches an extension's
266				// declared activation events. The extension then
267				// registers its command during activation, and the
268				// second registry lookup succeeds. Without this flow,
269				// any extension that gates on `onCommand:<id>` (e.g.
270				// GitLens' primary commands, Roo-Cline's commands, Vim
271				// mode toggles) never activates in response to a user
272				// gesture - it just silently does nothing.
273				if LookupCommandContributingExtension(self, &CommandIdentifier) {
274					dev_log!(
275						"commands",
276						"[CommandProvider] Lazy activation for command '{}' - firing onCommand:{0}",
277						CommandIdentifier
278					);
279
280					let Event = format!("onCommand:{}", CommandIdentifier);
281
282					let ActivationResult = Client::SendRequest::Fn(
283						&"cocoon-main".to_string(),
284						"$activateByEvent".to_string(),
285						json!({ "activationEvent": Event }),
286						30_000,
287					)
288					.await;
289
290					if let Err(Error) = ActivationResult {
291						dev_log!(
292							"commands",
293							"warn: [CommandProvider] onCommand:{} activation failed: {}",
294							CommandIdentifier,
295							Error
296						);
297					}
298
299					// The registerCommand channel-drain delivers within ~16 ms.
300					// Yield for one frame so the batch flush lands before
301					// the registry re-read below.
302					tokio::time::sleep(std::time::Duration::from_millis(20)).await;
303
304					let PostActivationHandler = self
305						.ApplicationState
306						.Extension
307						.Registry
308						.CommandRegistry
309						.lock()
310						.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
311						.get(&CommandIdentifier)
312						.cloned();
313
314					if let Some(Handler) = PostActivationHandler {
315						match Handler {
316							CommandHandler::Native(Function) => {
317								let MainWindow =
318									self.ApplicationHandle.get_webview_window("main").ok_or_else(|| {
319										CommonError::IPCError {
320											Description:"Could not find main window for lazy-activated native command"
321												.to_string(),
322										}
323									})?;
324
325								let RunTime =
326									self.ApplicationHandle.try_state::<Arc<ApplicationRunTime>>().ok_or_else(|| {
327										CommonError::IPCError {
328											Description:"ApplicationRunTime unavailable for lazy-activated native \
329											             command"
330												.to_string(),
331										}
332									})?;
333
334								return Function(
335									self.ApplicationHandle.clone(),
336									MainWindow,
337									(*RunTime).clone(),
338									Argument,
339								)
340								.await
341								.map_err(|Error| CommonError::CommandExecution { CommandIdentifier, Reason:Error });
342							},
343
344							CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier: ProxiedId } => {
345								let RPCParameters = json!([ProxiedId, Argument]);
346
347								let RPCMethod = format!(
348									"{}$ExecuteContributedCommand",
349									ProxyTarget::ExtHostCommands.GetTargetPrefix()
350								);
351
352								return Client::SendRequest::Fn(&SideCarIdentifier, RPCMethod, RPCParameters, 30_000)
353									.await
354									.map_err(|Error| CommonError::IPCError { Description:Error.to_string() });
355							},
356						}
357					}
358				}
359
360				dev_log!(
361					"commands",
362					"error: [CommandProvider] Command '{}' not found in registry.",
363					CommandIdentifier
364				);
365
366				Err(CommonError::CommandNotFound { Identifier:CommandIdentifier })
367			},
368		}
369	}
370
371	/// Registers a command contributed by a sidecar process.
372	async fn RegisterCommand(&self, SideCarIdentifier:String, CommandIdentifier:String) -> Result<(), CommonError> {
373		dev_log!(
374			"commands",
375			"[CommandProvider] Registering PROXY command '{}' from sidecar '{}'",
376			CommandIdentifier,
377			SideCarIdentifier
378		);
379
380		let mut Registry = self
381			.ApplicationState
382			.Extension
383			.Registry
384			.CommandRegistry
385			.lock()
386			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
387
388		Registry.insert(
389			CommandIdentifier.clone(),
390			CommandHandler::Proxied { SideCarIdentifier, CommandIdentifier },
391		);
392
393		Ok(())
394	}
395
396	/// Unregisters a previously registered command.
397	async fn UnregisterCommand(&self, _SideCarIdentifier:String, CommandIdentifier:String) -> Result<(), CommonError> {
398		dev_log!("commands", "[CommandProvider] Unregistering command '{}'", CommandIdentifier);
399
400		self.ApplicationState
401			.Extension
402			.Registry
403			.CommandRegistry
404			.lock()
405			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
406			.remove(&CommandIdentifier);
407
408		Ok(())
409	}
410
411	/// Gets a list of all currently registered command IDs.
412	async fn GetAllCommands(&self) -> Result<Vec<String>, CommonError> {
413		dev_log!("commands", "[CommandProvider] Getting all command identifiers.");
414
415		let Registry = self
416			.ApplicationState
417			.Extension
418			.Registry
419			.CommandRegistry
420			.lock()
421			.map_err(super::Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
422
423		Ok(Registry.keys().cloned().collect())
424	}
425}
426
427/// Return `true` when some scanned extension declares
428/// `onCommand:<CommandIdentifier>` as one of its activation events. Used
429/// by the lazy-activation fallback in `ExecuteCommand` - without this
430/// check we'd fire an `$activateByEvent("onCommand:X")` for every
431/// unknown command, which would cause Cocoon to log "no extension
432/// matching event" for every typo. Scans the cached registry; no IPC.
433fn LookupCommandContributingExtension(Environment:&MountainEnvironment, CommandIdentifier:&str) -> bool {
434	let Event = format!("onCommand:{}", CommandIdentifier);
435
436	let Guard = match Environment
437		.ApplicationState
438		.Extension
439		.ScannedExtensions
440		.ScannedExtensions
441		.lock()
442	{
443		Ok(G) => G,
444
445		Err(_) => return false,
446	};
447
448	for Description in Guard.values() {
449		if let Some(Events) = &Description.ActivationEvents {
450			if Events.iter().any(|E| E == &Event) {
451				return true;
452			}
453		}
454	}
455
456	false
457}