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
52	pub Name:String,
53
54	pub Index:usize,
55}
56
57impl From<&WorkspaceFolderStateDTO> for WorkspaceFolderPayload {
58	fn from(Dto:&WorkspaceFolderStateDTO) -> Self {
59		Self { Uri:Dto.URI.to_string(), Name:Dto.Name.clone(), Index:Dto.Index }
60	}
61}
62
63/// Open one or more workspace folders, replacing any currently-open set.
64///
65/// Returns the new folder list so Sky can update its sidebar without a
66/// round-trip. The caller should pass absolute filesystem paths; URLs
67/// are accepted and parsed, but Tauri dialog results are typically paths.
68#[tauri::command]
69pub async fn MountainWorkspaceOpenFolder(
70	app_handle:AppHandle,
71
72	state:State<'_, Arc<ApplicationState>>,
73
74	paths:Vec<String>,
75) -> Result<Vec<WorkspaceFolderPayload>, String> {
76	if paths.is_empty() {
77		return Err("No paths provided".to_string());
78	}
79
80	let mut Folders:Vec<WorkspaceFolderStateDTO> = Vec::with_capacity(paths.len());
81
82	for (Index, Raw) in paths.iter().enumerate() {
83		let Uri = if Raw.starts_with("file:") {
84			Url::parse(Raw).map_err(|Error| format!("Invalid file URL {}: {}", Raw, Error))?
85		} else {
86			let Path = PathBuf::from(Raw);
87
88			if !Path.is_dir() {
89				return Err(format!("Not a directory: {}", Path.display()));
90			}
91
92			let Canonical = Path.canonicalize().unwrap_or(Path.clone());
93
94			Url::from_directory_path(&Canonical)
95				.map_err(|()| format!("Failed to build directory URL for {}", Canonical.display()))?
96		};
97
98		let Name = PathBuf::from(Raw)
99			.file_name()
100			.and_then(|N| N.to_str())
101			.map(str::to_string)
102			.unwrap_or_else(|| Raw.clone());
103
104		Folders.push(WorkspaceFolderStateDTO::New(Uri, Name, Index)?);
105	}
106
107	UpdateWorkspaceFoldersAndBroadcast(&app_handle, &state.Workspace, Folders.clone());
108
109	dev_log!(
110		"lifecycle",
111		"[WorkspaceFolderCommand] Opened {} folder(s); first URI={}",
112		Folders.len(),
113		Folders.first().map(|F| F.URI.as_str()).unwrap_or("")
114	);
115
116	Ok(Folders.iter().map(WorkspaceFolderPayload::from).collect())
117}
118
119/// Return the current workspace folder set without mutating anything.
120#[tauri::command]
121pub async fn MountainWorkspaceListFolders(
122	state:State<'_, Arc<ApplicationState>>,
123) -> Result<Vec<WorkspaceFolderPayload>, String> {
124	Ok(state
125		.Workspace
126		.GetWorkspaceFolders()
127		.iter()
128		.map(WorkspaceFolderPayload::from)
129		.collect())
130}
131
132/// Close every workspace folder - equivalent to VS Code's
133/// `workbench.action.closeFolder`. Extensions that subscribe to
134/// `onDidChangeWorkspaceFolders` receive an event whose `removed` array
135/// contains every previously-open folder.
136#[tauri::command]
137pub async fn MountainWorkspaceCloseAllFolders(
138	app_handle:AppHandle,
139
140	state:State<'_, Arc<ApplicationState>>,
141) -> Result<Value, String> {
142	UpdateWorkspaceFoldersAndBroadcast(&app_handle, &state.Workspace, Vec::new());
143
144	dev_log!("lifecycle", "[WorkspaceFolderCommand] All folders closed");
145
146	Ok(Value::Null)
147}