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
42				crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
43					&run_time.Environment,
44					"sky://workspace/applyEdit",
45					Payload,
46				)
47				.await
48				.map_err(|Error| {
49					dev_log!("ipc", "error: [applyEdit] Sky did not answer ({:?})", Error);
50
51					Error.to_string()
52				})
53			})
54		},
55
56		"showTextDocument" => {
57			crate::effect!(run_time, {
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
78						Ok(json!(null))
79					},
80				}
81			})
82		},
83
84		// `editor.revealRange(range, revealType)` - scroll the Monaco editor to
85		// bring a range into view. Extensions use this for go-to-definition
86		// "reveal cursor", reference highlights, error navigation, etc.
87		// Routes to Sky's ICodeEditorService so Monaco scrolls its viewport.
88		"window.revealRange" => {
89			crate::effect!(run_time, {
90				match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
91					&run_time.Environment,
92					"sky://editor/revealRange",
93					Parameters,
94				)
95				.await
96				{
97					Ok(V) => Ok(V),
98					Err(_) => Ok(json!(null)),
99				}
100			})
101		},
102
103		// Workspace-trust family. vscode.git's `Model.openRepository` calls
104		// `await workspace.requestResourceTrust({uri, message})` and
105		// `await workspace.isResourceTrusted(uri)` before constructing the
106		// Repository. The Cocoon `WrapWorkspaceNamespace` Proxy fallback
107		// already returns a permissive `true` heuristic so vscode.git
108		// proceeds; routing the same method names through Mountain here
109		// gives the canonical handler a place to live (and makes
110		// `MountainMethods` see them via `GenerateRouteManifest.sh`'s grep,
111		// which switches the Cocoon shim from heuristic-default to
112		// gRPC-routed automatically on the next manifest regeneration). A
113		// future round can replace the unconditional `true` with a real
114		// per-OS trust query (Gatekeeper / SmartScreen / xattrs); single-
115		// window dev runtime stays trust-by-default.
116		"Workspace.RequestResourceTrust" | "Workspace.IsResourceTrusted" => {
117			crate::effect!(_run_time, { Ok(json!({ "trusted": true })) })
118		},
119
120		"$updateWorkspaceFolders" => {
121			crate::effect!(run_time, {
122				let Payload = array_unwrap(Parameters);
123
124				let Additions:Vec<(String, String)> = Payload
125					.get("additions")
126					.and_then(Value::as_array)
127					.map(|Array| {
128						Array
129							.iter()
130							.filter_map(|Entry| {
131								let Uri = Entry
132									.get("uri")
133									.and_then(|U| U.get("value").and_then(Value::as_str).or_else(|| U.as_str()))
134									.map(str::to_string)?;
135
136								let Name = Entry.get("name").and_then(Value::as_str).unwrap_or("").to_string();
137
138								Some((Uri, Name))
139							})
140							.collect()
141					})
142					.unwrap_or_default();
143
144				let Removals:Vec<String> = Payload
145					.get("removals")
146					.and_then(Value::as_array)
147					.map(|Array| {
148						Array
149							.iter()
150							.filter_map(|Entry| {
151								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							})
156							.collect()
157					})
158					.unwrap_or_default();
159
160				let Workspace = &run_time.Environment.ApplicationState.Workspace;
161
162				let mut Folders = Workspace.GetWorkspaceFolders();
163
164				Folders.retain(|F| !Removals.contains(&F.URI.to_string()));
165
166				let Base = Folders.len();
167
168				for (Index, (UriStr, Name)) in Additions.iter().enumerate() {
169					if let Ok(Url) = url::Url::parse(UriStr) {
170						if let Ok(Dto) =
171							crate::ApplicationState::DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO::New(
172								Url,
173								Name.clone(),
174								Base + Index,
175							) {
176							Folders.push(Dto);
177						}
178					}
179				}
180
181				crate::ApplicationState::State::WorkspaceState::WorkspaceDelta::UpdateWorkspaceFoldersAndNotify(
182					Workspace, Folders,
183				);
184
185				Ok(json!(null))
186			})
187		},
188
189		// `workspace.save(uri)` - Cocoon's shim calls this when an extension
190		// calls `vscode.workspace.save(uri)`. Route through Sky so the workbench's
191		// `ITextFileService.save(uri)` can flush the dirty working copy to disk.
192		// Returns the URI on success so the caller can confirm the file was saved.
193		"Workspace.Save" => {
194			crate::effect!(run_time, {
195				let UriVal = uri_from_params(Parameters);
196
197				// Fire `document.willSave` to Cocoon BEFORE writing to disk.
198				// This gives `onWillSaveTextDocument` listeners a chance to
199				// apply last-minute edits (format-on-save, organize-imports,
200				// trailing-whitespace strippers, etc.).
201				// Fire-and-forget with a short grace period so slow listeners
202				// don't stall the save for more than 1.5 s.
203				let WillSavePayload = serde_json::json!({
204					"uri": UriVal,
205					"reason": 1, // TextDocumentSaveReason.Manual
206				});
207
208				let _ = tokio::time::timeout(
209					std::time::Duration::from_millis(1500),
210					::Vine::Client::SendNotification::Fn(
211						"cocoon-main".to_string(),
212						"document.willSave".to_string(),
213						WillSavePayload,
214					),
215				)
216				.await;
217
218				let SaveResult = match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
219					&run_time.Environment,
220					"sky://workspace/save",
221					UriVal.clone(),
222				)
223				.await
224				{
225					Ok(Result) => {
226						if Result.is_null() {
227							UriVal.clone()
228						} else {
229							Result
230						}
231					},
232					Err(Error) => {
233						dev_log!("ipc", "warn: [Workspace.Save] Sky did not answer ({:?}); ok", Error);
234
235						UriVal.clone()
236					},
237				};
238
239				// Notify Cocoon that the file was saved so `onDidSaveTextDocument`
240				// fires for extension-triggered saves (format-on-save, etc.).
241				let _ = ::Vine::Client::SendNotification::Fn(
242					"cocoon-main".to_string(),
243					"$acceptModelSaved".to_string(),
244					serde_json::json!({ "uri": UriVal }),
245				)
246				.await;
247
248				Ok(SaveResult)
249			})
250		},
251
252		// `workspace.saveAs(uri)` - same as Save but opens a Save-As dialog.
253		// Currently delegates to the same Save path; a future Sky-side handler
254		// can drive the dialog independently.
255		"Workspace.SaveAs" => {
256			crate::effect!(run_time, {
257				let UriVal = uri_from_params(Parameters);
258
259				match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
260					&run_time.Environment,
261					"sky://workspace/saveAs",
262					UriVal.clone(),
263				)
264				.await
265				{
266					Ok(Result) => Ok(if Result.is_null() { UriVal } else { Result }),
267					Err(_) => Ok(UriVal),
268				}
269			})
270		},
271
272		// `saveAll` - Cocoon's older API surface calls this from `gRPC/Client.ts`
273		// when the workbench wants to flush all dirty working copies. Routes to
274		// Sky's `sky://workspace/saveAll` handler which delegates to VS Code's
275		// `ITextFileService.save({ saveReason: AutoSave })` for all dirty models.
276		"saveAll" | "Workspace.SaveAll" => {
277			crate::effect!(run_time, {
278				match crate::Environment::UserInterfaceProvider::SendUserInterfaceRequest(
279					&run_time.Environment,
280					"sky://workspace/saveAll",
281					serde_json::json!({}),
282				)
283				.await
284				{
285					Ok(Result) => Ok(Result),
286					Err(Error) => {
287						dev_log!("ipc", "warn: [saveAll] Sky did not answer ({:?}); ok", Error);
288
289						Ok(serde_json::json!(null))
290					},
291				}
292			})
293		},
294
295		_ => None,
296	}
297}