Skip to content

Commit 684cb80

Browse files
feat(Track): Implement command registration and execution handlers
- Added `commands.rs` handler module to manage Mountain's command registry in `AppState` - Implemented RPC handlers for `commands_registerCommand`, `commands_unregisterCommand`, and `commands_getCommands` to support Cocoon extension command registration - Created `handle_execute_command` router that dispatches to native handlers (e.g. `workbench.action.files.saveAll`) or proxies execution to sidecars via Vine IPC - Added native command implementations for core functionality including file saving and about dialog - Established command ownership pattern foundation (with TODOs for full implementation) - Integrated with Mountain's effect system for native command execution through `AppRuntime` This enables VS Code extension commands to be registered and executed through Cocoon's shim layer while maintaining native command performance. The proxy system routes extension commands back to their owning sidecar process through Vine IPC, critical for maintaining extension compatibility in Path A architecture.
1 parent d2e0bdc commit 684cb80

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

Source/handlers/commands.rs

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
// ---------------------------------------------------------------------------------------------
2+
// Mountain Command Handlers (handlers/commands.rs)
3+
// --------------------------------------------------------------------------------------------
4+
// Implements the core logic for managing and executing commands within
5+
// Mountain, handling interactions originating from both the frontend (via
6+
// Track) and sidecars (via RPC/Vine/Track). It maintains the central command
7+
// registry.
8+
//
9+
// Responsibilities:
10+
// - Managing the command registry (stored in `AppState`) which tracks both
11+
// native Mountain commands and commands registered by sidecars (like Cocoon).
12+
// - Handling `$registerCommand` RPC calls from Cocoon: Registers a *proxy*
13+
// handler that forwards execution back to the originating sidecar via Vine.
14+
// - Handling `$unregisterCommand` RPC calls from Cocoon: Removes the
15+
// corresponding proxy handler if the requesting sidecar is the owner.
16+
// - Handling `$getCommands` RPC calls from Cocoon: Returns a combined list of
17+
// all registered native and proxied command IDs.
18+
// - Handling command execution requests (`handle_execute_command`, called by
19+
// Track):
20+
// - Looks up the command ID in the registry.
21+
// - If native: Executes the corresponding native logic (calling the stored
22+
// handler function).
23+
// - If proxied: Sends a request (`commands_executeContributedCommand`) via
24+
// Vine to the owning sidecar to execute the command there.
25+
// - Returns the result or error from the execution.
26+
// - Providing functions to register native commands during Mountain startup
27+
// (`register_native_command_internal`).
28+
// - Implementing the actual logic for native commands (e.g.,
29+
30+
// `handle_native_save_all`).
31+
//
32+
// Key Interactions:
33+
// - Interacts heavily with `AppState` to access/modify the `command_registry`.
34+
// - Called by `track::dispatch_sidecar_request` for RPC methods.
35+
// - Called by `track::dispatch_command` (indirectly via effect) for execution.
36+
// - Uses `vine::send_request` to proxy command execution back to sidecars.
37+
// - Executes native command logic directly or via `ActionEffect`s using
38+
// `AppRuntime`.
39+
// --------------------------------------------------------------------------------------------
40+
41+
use std::{
42+
collections::HashMap,
43+
44+
future::Future,
45+
46+
pin::Pin,
47+
48+
// Use standard Mutex
49+
sync::{Arc, Mutex as StdMutex, MutexGuard},
50+
};
51+
52+
// Assume Land_Common provides necessary effects if used by native handlers
53+
use Land_Common::{command_effects, ui_effects, workspace_effects};
54+
// Use log crate for logging
55+
use log;
56+
use serde_json::{Value, json};
57+
use tauri::{AppHandle, Manager, Runtime as TauriRuntime, State, Window};
58+
59+
use crate::{
60+
// Import AppState and CommandHandler enum
61+
app_state::{AppState, CommandHandler},
62+
63+
runtime::AppRuntime,
64+
65+
// Track might not be needed directly if only called by it
66+
track,
67+
68+
// Vine required for proxying back to sidecars
69+
vine,
70+
};
71+
72+
// --- Helper Functions ---
73+
74+
/// Helper to create a structured error JSON string for handler results.
75+
fn create_handler_error_string(message:String, code:Option<&str>) -> String {
76+
json!({ "message": message, "code": code.unwrap_or("EUNKNOWN") }).to_string()
77+
}
78+
79+
/// Helper to map Mutex lock poisoning errors to the handler's error string
80+
/// format.
81+
fn map_lock_error<T>(e:std::sync::PoisonError<MutexGuard<'_, T>>) -> String {
82+
create_handler_error_string(format!("Failed to acquire lock on command registry: {}", e), Some("ELOCKED"))
83+
}
84+
85+
// --- Request Handlers (Called by Track dispatcher for sidecar requests) ---
86+
87+
/// Handles the `commands_registerCommand` request from a sidecar shim.
88+
/// Stores the association between the sidecar and the registered command ID.
89+
pub async fn handle_register_command<R:TauriRuntime>(
90+
app:AppHandle<R>,
91+
92+
sidecar_id:String,
93+
94+
params:Value,
95+
) -> Result<Value, String> {
96+
let command_id = params
97+
.get("id")
98+
.and_then(|v| v.as_str())
99+
.ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))?
100+
.to_string();
101+
102+
// Keep: Essential registration action log
103+
log::info!("[Cmd Handler] Registering PROXY for '{}' from '{}'", command_id, sidecar_id);
104+
105+
let app_state = app.state::<AppState>();
106+
107+
let mut registry = app_state.command_registry.lock().map_err(map_lock_error)?;
108+
109+
// Keep: Warning for overwrite is useful
110+
if registry.contains_key(&command_id) {
111+
log::warn!(
112+
"[Cmd Handler] Warning: Command ID '{}' is already registered. Overwriting.",
113+
command_id
114+
);
115+
116+
// TODO: Add ownership tracking and update/remove old owner if
117+
// overwriting
118+
}
119+
120+
// Insert the proxy handler
121+
registry.insert(
122+
command_id.clone(),
123+
CommandHandler::Proxied { sidecar_id:sidecar_id.clone(), command_id:command_id.clone() },
124+
);
125+
126+
// Keep: Confirmation of success
127+
log::info!("[Cmd Handler] Command '{}' registered successfully in AppState.", command_id);
128+
129+
// TODO: Register ownership if implemented
130+
131+
// Success, void operation
132+
Ok(Value::Null)
133+
}
134+
135+
/// Handles the `commands_unregisterCommand` request from a sidecar shim.
136+
/// Removes the command ID association for the originating sidecar (if owned).
137+
pub async fn handle_unregister_command<R:TauriRuntime>(
138+
app:AppHandle<R>,
139+
140+
sidecar_id:String,
141+
142+
params:Value,
143+
) -> Result<Value, String> {
144+
let command_id = params
145+
.get("id")
146+
.and_then(|v| v.as_str())
147+
.ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))?;
148+
149+
// Keep: Essential unregistration action log
150+
log::info!(
151+
"[Cmd Handler] Unregistering command '{}' from sidecar '{}'",
152+
command_id,
153+
sidecar_id
154+
);
155+
156+
let app_state = app.state::<AppState>();
157+
158+
let mut registry = app_state.command_registry.lock().map_err(map_lock_error)?;
159+
160+
// TODO: Implement ownership check before removing.
161+
// For now, simple removal:
162+
// Keep: Confirmation or warning log
163+
if registry.remove(command_id).is_some() {
164+
log::info!("[Cmd Handler] Command '{}' unregistered.", command_id);
165+
166+
// TODO: Also remove from ownership map if implemented
167+
} else {
168+
log::warn!("[Cmd Handler] Command '{}' not found for unregistration.", command_id);
169+
}
170+
171+
// Success, void operation even if not found
172+
Ok(Value::Null)
173+
}
174+
175+
/// Handles the `commands_getCommands` request from a sidecar shim.
176+
/// Returns a list of *all* registered command IDs (native and proxied).
177+
pub async fn handle_get_commands<R:TauriRuntime>(
178+
app:AppHandle<R>,
179+
180+
// Keep runtime state injection if needed later
181+
_runtime:State<'_, Arc<AppRuntime>>,
182+
) -> Result<Value, String> {
183+
// Reduced logging: log::trace!("[Cmd Handler] Handling getCommands request");
184+
185+
let app_state = app.state::<AppState>();
186+
187+
let registry = app_state.command_registry.lock().map_err(map_lock_error)?;
188+
189+
let command_list:Vec<String> = registry.keys().cloned().collect();
190+
191+
// Release lock
192+
drop(registry);
193+
194+
Ok(json!(command_list))
195+
}
196+
197+
/// Handles the `commands_executeCommand` request, typically from a sidecar shim
198+
/// or the frontend. Determines if the command is native or registered by a
199+
/// sidecar and routes accordingly.
200+
pub async fn handle_execute_command<R:TauriRuntime>(
201+
app:AppHandle<R>,
202+
203+
// Window context might be needed for native actions
204+
window:Window<R>,
205+
206+
// Runtime needed for executing native effects
207+
runtime:State<'_, Arc<AppRuntime>>,
208+
209+
// Expects { "id": "command.id", "args": [...] }
210+
params:Value,
211+
) -> Result<Value, String> {
212+
let command_id = params
213+
.get("id")
214+
.and_then(|v| v.as_str())
215+
.ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))?
216+
.to_string();
217+
218+
// Default to Null if missing
219+
let args = params.get("args").cloned().unwrap_or(Value::Null);
220+
221+
// Keep: Essential action log
222+
log::info!("[Cmd Handler] Handling executeCommand '{}'", command_id);
223+
224+
let app_state = app.state::<AppState>();
225+
226+
// Get handler info while holding lock briefly
227+
let handler_info = {
228+
let registry = app_state.command_registry.lock().map_err(map_lock_error)?;
229+
230+
registry.get(&command_id).cloned()
231+
// Lock released
232+
};
233+
234+
match handler_info {
235+
Some(CommandHandler::Native(native_handler_fn)) => {
236+
// Keep: Distinguishes native execution path
237+
log::debug!("[Cmd Handler] Found Native handler for '{}'. Executing...", command_id);
238+
239+
native_handler_fn(app, window, runtime.inner().clone(), args).await
240+
},
241+
242+
Some(CommandHandler::Proxied { sidecar_id, command_id: proxied_cmd_id }) => {
243+
// Keep: Distinguishes proxied execution path
244+
log::debug!(
245+
"[Cmd Handler] Found Proxied handler for '{}'. Routing execution to sidecar '{}'",
246+
proxied_cmd_id,
247+
sidecar_id
248+
);
249+
250+
let request_payload = json!({ "id": proxied_cmd_id, "args": args });
251+
252+
let sidecar_method = "commands_executeContributedCommand".to_string();
253+
254+
vine::send_request(&sidecar_id, sidecar_method, request_payload, false, 30000)
255+
.await
256+
.map_err(|e| {
257+
log::error!(
258+
"[Cmd Handler] Vine error routing command '{}' to sidecar '{}': {}",
259+
command_id,
260+
sidecar_id,
261+
e
262+
);
263+
264+
create_handler_error_string(
265+
format!("Failed to route command '{}' to sidecar '{}'", command_id, sidecar_id),
266+
Some("EIPC"),
267+
)
268+
})
269+
},
270+
271+
None => {
272+
// Keep: Important error log
273+
log::error!("[Cmd Handler] Command '{}' not found in registry.", command_id);
274+
275+
// Return structured error string
276+
Err(create_handler_error_string(
277+
format!("Command '{}' not found.", command_id),
278+
Some("ENOCMD"),
279+
// Example custom code
280+
))
281+
},
282+
}
283+
}
284+
285+
// --- Native Command Handler Implementations ---
286+
287+
/// Native handler for `workbench.action.files.saveAll`. Runs the effect.
288+
pub fn handle_native_save_all<R:TauriRuntime>(
289+
_app:AppHandle<R>,
290+
291+
_window:Window<R>,
292+
293+
runtime:Arc<AppRuntime>,
294+
295+
args:Value,
296+
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
297+
Box::pin(async move {
298+
let include_untitled = args.get(0).and_then(|v| v.as_bool()).unwrap_or(true);
299+
300+
log::info!("[Native Cmd] Executing saveAll (includeUntitled: {})", include_untitled);
301+
302+
let effect = workspace_effects::save_all(include_untitled);
303+
304+
runtime.run(effect).await.map(|_| Value::Null).map_err(|e| e.to_string())
305+
})
306+
}
307+
308+
/// Native handler for `mountain.action.showAbout`. Runs the effect.
309+
pub fn handle_native_show_about<R:TauriRuntime>(
310+
_app:AppHandle<R>,
311+
312+
_window:Window<R>,
313+
314+
runtime:Arc<AppRuntime>,
315+
316+
_args:Value,
317+
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
318+
Box::pin(async move {
319+
log::info!("[Native Cmd] Executing showAbout");
320+
321+
let message = format!("Land Editor (Mountain)\nVersion: {}\nMore info...", env!("CARGO_PKG_VERSION"));
322+
323+
let effect = ui_effects::show_message("info".to_string(), message, None);
324+
325+
runtime.run(effect).await.map(|_| Value::Null).map_err(|e| e.to_string())
326+
})
327+
}
328+
329+
/// Placeholder native handler function.
330+
pub fn native_placeholder_handler<R:TauriRuntime>(
331+
_app:AppHandle<R>,
332+
333+
_window:Window<R>,
334+
335+
_runtime:Arc<AppRuntime>,
336+
337+
args:Value,
338+
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
339+
Box::pin(async move {
340+
// Keep: Useful for seeing if placeholder was called
341+
log::info!("[Native Cmd Placeholder] Executed with args: {:?}", args);
342+
343+
Ok(json!("Native Placeholder OK"))
344+
})
345+
}
346+
347+
// --- Native Command Registration Helper ---
348+
349+
/// Helper to register a native command handler map during startup (e.g., in
350+
/// `app_state.rs`).
351+
pub fn register_native_command_internal<R:TauriRuntime + 'static>(
352+
registry:&mut HashMap<String, CommandHandler<R>>,
353+
354+
command_id:String,
355+
356+
handler:fn(
357+
AppHandle<R>,
358+
359+
Window<R>,
360+
361+
Arc<AppRuntime>,
362+
363+
Value,
364+
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>>,
365+
) {
366+
// Keep: Warning for collisions
367+
if registry.contains_key(&command_id) {
368+
log::warn!(
369+
"[Cmd Registry Init] Warning: Native command ID '{}' collision during registration.",
370+
command_id
371+
);
372+
}
373+
374+
// Keep: Log registration during init
375+
log::info!("[Cmd Registry Init] Registered native command: {}", command_id);
376+
377+
registry.insert(command_id, CommandHandler::Native(handler));
378+
}

0 commit comments

Comments
 (0)