Skip to main content

Mountain/Binary/IPC/
WorkspaceFolderCommand.rs

1//! # WorkspaceFolderCommand
2//!
3//! Tauri commands for opening, listing, and closing workspace folders at
4//! runtime. These are the second half of Plan BATCH-02: the first half
5//! (autoload at startup) seeds `ApplicationState.Workspace.WorkspaceFolders`
6//! from CLI / env. These commands let Sky drive the same state change
7//! after boot, for a welcome-screen "Open Folder" button or a recent-files
8//! picker.
9//!
10//! ## Flow
11//!
12//! ```text
13//! Sky clicks "Open Folder" ──invoke──> MountainWorkspaceOpenFolder
14//!                                           │
15//!                                           ▼
16//!             ApplicationState.Workspace.SetWorkspaceFolders(...)
17//!                                           │
18//!                                           ├── UpdateWorkspaceFoldersRequest
19//!                                           ▼        (to Cocoon via gRPC)
20//!           extensions see new `vscode.workspace.workspaceFolders`
21//!           and receive `onDidChangeWorkspaceFolders`.
22//! ```
23//!
24//! The command deliberately validates the path before touching state: a
25//! non-existent directory (user fat-fingered a drag, for instance) returns
26//! an error and the existing folder set is untouched.
27
28use std::{path::PathBuf, sync::Arc};
29
30use serde::{Deserialize, Serialize};
31use serde_json::Value;
32use tauri::{AppHandle, State};
33use url::Url;
34
35use crate::{
36	ApplicationState::{
37		DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO,
38		State::{
39			ApplicationState::ApplicationState,
40			WorkspaceState::WorkspaceDelta::UpdateWorkspaceFoldersAndBroadcast,
41		},
42	},
43	dev_log,
44};
45
46/// JSON-serialisable record returned to Sky for every folder in the set.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct WorkspaceFolderPayload {
50	pub Uri:String,
51	pub Name:String,
52	pub Index:usize,
53}
54
55impl From<&WorkspaceFolderStateDTO> for WorkspaceFolderPayload {
56	fn from(Dto:&WorkspaceFolderStateDTO) -> Self {
57		Self { Uri:Dto.URI.to_string(), Name:Dto.Name.clone(), Index:Dto.Index }
58	}
59}
60
61/// Open one or more workspace folders, replacing any currently-open set.
62///
63/// Returns the new folder list so Sky can update its sidebar without a
64/// round-trip. The caller should pass absolute filesystem paths; URLs
65/// are accepted and parsed, but Tauri dialog results are typically paths.
66#[tauri::command]
67pub async fn MountainWorkspaceOpenFolder(
68	app_handle:AppHandle,
69	state:State<'_, Arc<ApplicationState>>,
70	paths:Vec<String>,
71) -> Result<Vec<WorkspaceFolderPayload>, String> {
72	if paths.is_empty() {
73		return Err("No paths provided".to_string());
74	}
75
76	let mut Folders:Vec<WorkspaceFolderStateDTO> = Vec::with_capacity(paths.len());
77	for (Index, Raw) in paths.iter().enumerate() {
78		let Uri = if Raw.starts_with("file:") {
79			Url::parse(Raw).map_err(|Error| format!("Invalid file URL {}: {}", Raw, Error))?
80		} else {
81			let Path = PathBuf::from(Raw);
82			if !Path.is_dir() {
83				return Err(format!("Not a directory: {}", Path.display()));
84			}
85			let Canonical = Path.canonicalize().unwrap_or(Path.clone());
86			Url::from_directory_path(&Canonical)
87				.map_err(|()| format!("Failed to build directory URL for {}", Canonical.display()))?
88		};
89		let Name = PathBuf::from(Raw)
90			.file_name()
91			.and_then(|N| N.to_str())
92			.map(str::to_string)
93			.unwrap_or_else(|| Raw.clone());
94		Folders.push(WorkspaceFolderStateDTO::New(Uri, Name, Index)?);
95	}
96
97	UpdateWorkspaceFoldersAndBroadcast(&app_handle, &state.Workspace, Folders.clone());
98	dev_log!(
99		"lifecycle",
100		"[WorkspaceFolderCommand] Opened {} folder(s); first URI={}",
101		Folders.len(),
102		Folders.first().map(|F| F.URI.as_str()).unwrap_or("")
103	);
104
105	Ok(Folders.iter().map(WorkspaceFolderPayload::from).collect())
106}
107
108/// Return the current workspace folder set without mutating anything.
109#[tauri::command]
110pub async fn MountainWorkspaceListFolders(
111	state:State<'_, Arc<ApplicationState>>,
112) -> Result<Vec<WorkspaceFolderPayload>, String> {
113	Ok(state
114		.Workspace
115		.GetWorkspaceFolders()
116		.iter()
117		.map(WorkspaceFolderPayload::from)
118		.collect())
119}
120
121/// Close every workspace folder - equivalent to VS Code's
122/// `workbench.action.closeFolder`. Extensions that subscribe to
123/// `onDidChangeWorkspaceFolders` receive an event whose `removed` array
124/// contains every previously-open folder.
125#[tauri::command]
126pub async fn MountainWorkspaceCloseAllFolders(
127	app_handle:AppHandle,
128	state:State<'_, Arc<ApplicationState>>,
129) -> Result<Value, String> {
130	UpdateWorkspaceFoldersAndBroadcast(&app_handle, &state.Workspace, Vec::new());
131	dev_log!("lifecycle", "[WorkspaceFolderCommand] All folders closed");
132	Ok(Value::Null)
133}