Skip to main content

Mountain/Track/Effect/CreateEffectForRequest/
Workspace.rs

1#![allow(unused_variables, dead_code, unused_imports)]
2
3//! # Workspace Effect (CreateEffectForRequest)
4//!
5//! Effect constructors for workspace-level RPC methods. Handles:
6//! - `applyEdit` and `showTextDocument` via round-trip to Sky through
7//!   `UserInterfaceProvider::SendUserInterfaceRequest` (resolves when Sky has
8//!   actually applied the edit or shown the document).
9//! - `Workspace.RequestResourceTrust` and `Workspace.IsResourceTrusted` return
10//!   a permissive `true` heuristic so `vscode.git` proceeds; single- window dev
11//!   runtime stays trust-by-default.
12//! - `$updateWorkspaceFolders` applies workspace folder additions/removals to
13//!   `ApplicationState.Workspace` and broadcasts the delta.
14
15use std::{future::Future, pin::Pin, sync::Arc};
16
17use serde_json::{Value, json};
18use tauri::Runtime;
19
20use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, Track::Effect::MappedEffectType::MappedEffect, dev_log};
21
22pub fn CreateEffect<R:Runtime>(MethodName:&str, Parameters:Value) -> Option<Result<MappedEffect, String>> {
23	match MethodName {
24		"applyEdit" => {
25			let effect =
26				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
27					Box::pin(async move {
28						// Atom T1: round-trip via Mountain's request/reply plumbing so the
29						// extension's `await workspace.applyEdit(…)` resolves when Sky has
30						// actually applied the edit (or refused). Previously a synthetic
31						// `true` returned before the edit ran, racing listeners that
32						// expected post-apply state.
33						let Payload = if Parameters.is_array() {
34							Parameters.get(0).cloned().unwrap_or_default()
35						} else {
36							Parameters
37						};
38						crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
39							&run_time.Environment,
40							"sky://workspace/applyEdit",
41							Payload,
42						)
43						.await
44						.map_err(|Error| {
45							dev_log!("ipc", "error: [applyEdit] Sky did not answer ({:?})", Error);
46							Error.to_string()
47						})
48					})
49				};
50
51			Some(Ok(Box::new(effect)))
52		},
53
54		"showTextDocument" => {
55			let effect =
56				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
57					Box::pin(async move {
58						// Atom T1: same round-trip as applyEdit. The canonical vscode
59						// return shape is a `TextEditor` - today Sky resolves with a
60						// thin `{ uri, viewColumn }` stub. Extensions that chain
61						// editor ops may still see undefined properties; that's a
62						// Sky-side enrichment task (T2 follow-up).
63						match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
64							&run_time.Environment,
65							"sky://window/showTextDocument",
66							Parameters,
67						)
68						.await
69						{
70							Ok(Value) => Ok(Value),
71							Err(Error) => {
72								dev_log!(
73									"ipc",
74									"warn: [showTextDocument] Sky did not answer ({:?}); returning null",
75									Error
76								);
77								Ok(json!(null))
78							},
79						}
80					})
81				};
82
83			Some(Ok(Box::new(effect)))
84		},
85
86		// `editor.revealRange(range, revealType)` - scroll the Monaco editor to
87		// bring a range into view. Extensions use this for go-to-definition
88		// "reveal cursor", reference highlights, error navigation, etc.
89		// Routes to Sky's ICodeEditorService so Monaco scrolls its viewport.
90		"window.revealRange" => {
91			let effect =
92				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
93					Box::pin(async move {
94						match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
95							&run_time.Environment,
96							"sky://editor/revealRange",
97							Parameters,
98						)
99						.await
100						{
101							Ok(V) => Ok(V),
102							Err(_) => Ok(json!(null)),
103						}
104					})
105				};
106
107			Some(Ok(Box::new(effect)))
108		},
109
110		// Workspace-trust family. vscode.git's `Model.openRepository` calls
111		// `await workspace.requestResourceTrust({uri, message})` and
112		// `await workspace.isResourceTrusted(uri)` before constructing the
113		// Repository. The Cocoon `WrapWorkspaceNamespace` Proxy fallback
114		// already returns a permissive `true` heuristic so vscode.git
115		// proceeds; routing the same method names through Mountain here
116		// gives the canonical handler a place to live (and makes
117		// `MountainMethods` see them via `GenerateRouteManifest.sh`'s grep,
118		// which switches the Cocoon shim from heuristic-default to
119		// gRPC-routed automatically on the next manifest regeneration). A
120		// future round can replace the unconditional `true` with a real
121		// per-OS trust query (Gatekeeper / SmartScreen / xattrs); single-
122		// window dev runtime stays trust-by-default.
123		"Workspace.RequestResourceTrust" | "Workspace.IsResourceTrusted" => {
124			let effect =
125				move |_run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
126
127					Box::pin(async move {
128						Ok(json!({ "trusted": true }))
129					})
130				};
131
132			Some(Ok(Box::new(effect)))
133		},
134
135		"$updateWorkspaceFolders" => {
136			let effect =
137				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
138					Box::pin(async move {
139						let Payload = if Parameters.is_array() {
140							Parameters.get(0).cloned().unwrap_or_default()
141						} else {
142							Parameters
143						};
144						let Additions:Vec<(String, String)> = Payload
145							.get("additions")
146							.and_then(Value::as_array)
147							.map(|Array| {
148								Array
149									.iter()
150									.filter_map(|Entry| {
151										let Uri = Entry
152											.get("uri")
153											.and_then(|U| U.get("value").and_then(Value::as_str).or_else(|| U.as_str()))
154											.map(str::to_string)?;
155										let Name = Entry.get("name").and_then(Value::as_str).unwrap_or("").to_string();
156										Some((Uri, Name))
157									})
158									.collect()
159							})
160							.unwrap_or_default();
161						let Removals:Vec<String> = Payload
162							.get("removals")
163							.and_then(Value::as_array)
164							.map(|Array| {
165								Array
166									.iter()
167									.filter_map(|Entry| {
168										Entry
169											.get("uri")
170											.and_then(|U| U.get("value").and_then(Value::as_str).or_else(|| U.as_str()))
171											.map(str::to_string)
172									})
173									.collect()
174							})
175							.unwrap_or_default();
176
177						let Workspace = &run_time.Environment.ApplicationState.Workspace;
178						let mut Folders = Workspace.GetWorkspaceFolders();
179						Folders.retain(|F| !Removals.contains(&F.URI.to_string()));
180						let Base = Folders.len();
181						for (Index, (UriStr, Name)) in Additions.iter().enumerate() {
182							if let Ok(Url) = url::Url::parse(UriStr) {
183								if let Ok(Dto) =
184									crate::ApplicationState::DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO::New(
185										Url,
186										Name.clone(),
187										Base + Index,
188									) {
189									Folders.push(Dto);
190								}
191							}
192						}
193						crate::ApplicationState::State::WorkspaceState::WorkspaceDelta::UpdateWorkspaceFoldersAndNotify(
194							Workspace, Folders,
195						);
196						Ok(json!(null))
197					})
198				};
199
200			Some(Ok(Box::new(effect)))
201		},
202
203		// `workspace.save(uri)` - Cocoon's shim calls this when an extension
204		// calls `vscode.workspace.save(uri)`. Route through Sky so the workbench's
205		// `ITextFileService.save(uri)` can flush the dirty working copy to disk.
206		// Returns the URI on success so the caller can confirm the file was saved.
207		"Workspace.Save" => {
208			let effect =
209				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
210					Box::pin(async move {
211						let UriVal = if Parameters.is_array() {
212							Parameters.get(0).cloned().unwrap_or_default()
213						} else {
214							Parameters.get("uri").cloned().unwrap_or(Parameters)
215						};
216
217						// Fire `document.willSave` to Cocoon BEFORE writing to disk.
218						// This gives `onWillSaveTextDocument` listeners a chance to
219						// apply last-minute edits (format-on-save, organize-imports,
220						// trailing-whitespace strippers, etc.).
221						// Fire-and-forget with a short grace period so slow listeners
222						// don't stall the save for more than 1.5 s.
223						let WillSavePayload = serde_json::json!({
224							"uri": UriVal,
225							"reason": 1, // TextDocumentSaveReason.Manual
226						});
227						let _ = tokio::time::timeout(
228							std::time::Duration::from_millis(1500),
229							crate::Vine::Client::SendNotification::Fn(
230								"cocoon-main".to_string(),
231								"document.willSave".to_string(),
232								WillSavePayload,
233							),
234						)
235						.await;
236
237						match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
238							&run_time.Environment,
239							"sky://workspace/save",
240							UriVal.clone(),
241						)
242						.await
243						{
244							Ok(Result) => Ok(if Result.is_null() { UriVal } else { Result }),
245							Err(Error) => {
246								dev_log!("ipc", "warn: [Workspace.Save] Sky did not answer ({:?}); ok", Error);
247								Ok(UriVal)
248							},
249						}
250					})
251				};
252
253			Some(Ok(Box::new(effect)))
254		},
255
256		// `workspace.saveAs(uri)` - same as Save but opens a Save-As dialog.
257		// Currently delegates to the same Save path; a future Sky-side handler
258		// can drive the dialog independently.
259		"Workspace.SaveAs" => {
260			let effect =
261				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
262					Box::pin(async move {
263						let UriVal = if Parameters.is_array() {
264							Parameters.get(0).cloned().unwrap_or_default()
265						} else {
266							Parameters.get("uri").cloned().unwrap_or(Parameters)
267						};
268						match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
269							&run_time.Environment,
270							"sky://workspace/saveAs",
271							UriVal.clone(),
272						)
273						.await
274						{
275							Ok(Result) => Ok(if Result.is_null() { UriVal } else { Result }),
276							Err(_) => Ok(UriVal),
277						}
278					})
279				};
280
281			Some(Ok(Box::new(effect)))
282		},
283
284		// `saveAll` - Cocoon's older API surface calls this from `gRPC/Client.ts`
285		// when the workbench wants to flush all dirty working copies. Routes to
286		// Sky's `sky://workspace/saveAll` handler which delegates to VS Code's
287		// `ITextFileService.save({ saveReason: AutoSave })` for all dirty models.
288		"saveAll" | "Workspace.SaveAll" => {
289			let effect =
290				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
291					Box::pin(async move {
292						match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
293							&run_time.Environment,
294							"sky://workspace/saveAll",
295							serde_json::json!({}),
296						)
297						.await
298						{
299							Ok(Result) => Ok(Result),
300							Err(Error) => {
301								dev_log!("ipc", "warn: [saveAll] Sky did not answer ({:?}); ok", Error);
302								Ok(serde_json::json!(null))
303							},
304						}
305					})
306				};
307
308			Some(Ok(Box::new(effect)))
309		},
310
311		_ => None,
312	}
313}