Skip to main content

Mountain/Track/Effect/CreateEffectForRequest/
Workspace.rs

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