Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Environment/
CustomEditorProvider.rs

1//! # CustomEditorProvider (Environment)
2//!
3//! Implements
4//! [`CustomEditorProvider`](CommonLibrary::CustomEditor::CustomEditorProvider)
5//! for `MountainEnvironment`, managing registration and lifecycle of custom
6//! non-text editors. Coordinates Webview-based editing experiences (SVG
7//! editors, diff viewers, etc.) and handles editor resolution, save
8//! operations, and provider unregistration.
9//!
10//! Uses [`IPCProvider`](CommonLibrary::IPC::IPCProvider) for RPC communication
11//! with Cocoon and integrates with `ApplicationState` for provider registration
12//! persistence.
13//!
14//! ## Methods
15//!
16//! - `RegisterCustomEditorProvider` - register extension provider by view type
17//! - `UnregisterCustomEditorProvider` - unregister provider
18//! - `OnSaveCustomDocument` - workbench → extension save reverse-RPC via
19//!   `$onSaveCustomDocument`; returns the sidecar's error verbatim on failure
20//! - `ResolveCustomEditor` - fire-and-forget RPC to populate the webview
21//!
22//! ## VS Code reference
23//!
24//! - `vs/workbench/contrib/customEditor/browser/customEditorService.ts`
25//! - `vs/workbench/contrib/customEditor/common/customEditor.ts`
26
27use std::{
28	collections::HashMap,
29	sync::{Arc, Mutex, OnceLock},
30};
31
32use CommonLibrary::{
33	CustomEditor::CustomEditorProvider::CustomEditorProvider,
34	Environment::Requires::Requires,
35	Error::CommonError::CommonError,
36	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
37};
38use async_trait::async_trait;
39use serde_json::{Value, json};
40use tauri::{Emitter, Manager};
41use url::Url;
42
43use super::MountainEnvironment::MountainEnvironment;
44use crate::{
45	RunTime::ApplicationRunTime::ApplicationRunTime,
46	Track::Effect::CreateEffectForRequest::Utilities::Proxy::proxy_cocoon,
47	dev_log,
48};
49
50/// Process-global custom editor registry: ViewType → SidecarId.
51/// Populated by `RegisterCustomEditorProvider`, consumed by
52/// `ResolveCustomEditor` to route the save/resolve RPC to the
53/// correct extension host sidecar.
54static CUSTOM_EDITOR_REGISTRY:OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
55
56fn GetRegistry() -> &'static Mutex<HashMap<String, String>> {
57	CUSTOM_EDITOR_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
58}
59
60/// Return the sidecar identifier for a registered ViewType, or
61/// `"cocoon-main"` as the canonical fallback.
62pub fn LookupSidecarForViewType(ViewType:&str) -> String {
63	GetRegistry()
64		.lock()
65		.ok()
66		.and_then(|G| G.get(ViewType).cloned())
67		.unwrap_or_else(|| "cocoon-main".to_string())
68}
69
70#[async_trait]
71impl CustomEditorProvider for MountainEnvironment {
72	async fn RegisterCustomEditorProvider(&self, ViewType:String, _Options:Value) -> Result<(), CommonError> {
73		dev_log!(
74			"extensions",
75			"[CustomEditorProvider] Registering provider for view type: {}",
76			ViewType
77		);
78
79		// Validate ViewType is non-empty
80		if ViewType.is_empty() {
81			return Err(CommonError::InvalidArgument {
82				ArgumentName:"ViewType".to_string(),
83				Reason:"ViewType cannot be empty".to_string(),
84			});
85		}
86
87		// Store ViewType → "cocoon-main" (the only extension host sidecar
88		// today). When Grove multi-extension-host lands, the sidecar id will
89		// come from the Options payload.
90		let SidecarId = _Options
91			.get("sidecarId")
92			.and_then(Value::as_str)
93			.unwrap_or("cocoon-main")
94			.to_string();
95
96		if let Ok(mut Registry) = GetRegistry().lock() {
97			let IsNew = !Registry.contains_key(&ViewType);
98
99			Registry.insert(ViewType.clone(), SidecarId.clone());
100
101			dev_log!(
102				"extensions",
103				"[CustomEditorProvider] {} provider registered: viewType={} sidecar={}",
104				if IsNew { "New" } else { "Updated" },
105				ViewType,
106				SidecarId
107			);
108		}
109
110		Ok(())
111	}
112
113	async fn UnregisterCustomEditorProvider(&self, ViewType:String) -> Result<(), CommonError> {
114		dev_log!(
115			"extensions",
116			"[CustomEditorProvider] Unregistering provider for view type: {}",
117			ViewType
118		);
119
120		if let Ok(mut Registry) = GetRegistry().lock() {
121			let Removed = Registry.remove(&ViewType).is_some();
122
123			dev_log!(
124				"extensions",
125				"[CustomEditorProvider] Provider unregistered: viewType={} (was_present={})",
126				ViewType,
127				Removed
128			);
129		}
130
131		Ok(())
132	}
133
134	async fn OnSaveCustomDocument(&self, ViewType:String, ResourceURI:Url) -> Result<(), CommonError> {
135		dev_log!(
136			"extensions",
137			"[CustomEditorProvider] OnSaveCustomDocument called for '{}' at '{}'",
138			ViewType,
139			ResourceURI
140		);
141
142		// Workbench → extension save reverse-RPC. Cocoon's
143		// `NotificationHandler.ts:781-810` already routes
144		// `$onSaveCustomDocument` to the `customEditor.saveDocument`
145		// emitter channel which fans out to whichever provider Cocoon's
146		// `WindowNamespace.ts:188+` subscribed via `Subscribe(...)` at
147		// `registerCustomEditorProvider` time. The extension's
148		// `saveCustomDocument(document, cancellationToken)` callback
149		// runs inside Cocoon - retrieves the edited content from the
150		// webview, returns a `Thenable<void>` once the file has been
151		// written. Mountain doesn't need to write the bytes itself; the
152		// extension does that via its existing `vscode.workspace.fs`
153		// shim which Cocoon already routes back into Mountain's
154		// `FileSystem.WriteFile` IPC.
155		//
156		// Wire shape mirrors VS Code's
157		// `vs/workbench/api/common/extHostCustom.ts::ExtHostCustomEditors`
158		// `$onSaveCustomDocument` handler which expects positional args
159		// `[CustomDocumentIdentifier, CancellationTokenId]`. Mountain
160		// sends the resource URI as the document identifier (extension
161		// stored the document under this key when it returned its
162		// `CustomDocument` from `openCustomDocument`); the cancellation
163		// token id is unused by our shim path and we send `0`.
164		let run_time:Arc<ApplicationRunTime> =
165			self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
166
167		let DocumentIdentifier = json!({
168			"viewType": ViewType,
169			"resource": { "external": ResourceURI.to_string() },
170		});
171
172		let RPCParameters = json!([DocumentIdentifier, 0]);
173
174		match proxy_cocoon(
175			&run_time,
176			ProxyTarget::ExtHostCustomEditors,
177			"onSaveCustomDocument",
178			RPCParameters,
179			30_000,
180		)
181		.await
182		.map_err(|e| CommonError::IPCError { Description:e })
183		{
184			Ok(_) => {
185				dev_log!(
186					"extensions",
187					"[CustomEditorProvider] OnSaveCustomDocument completed for '{}' at '{}'",
188					ViewType,
189					ResourceURI
190				);
191
192				let _ = self.ApplicationHandle.emit(
193					"sky://customEditor/saved",
194					json!({
195						"viewType": ViewType,
196						"resource": ResourceURI.to_string(),
197					}),
198				);
199
200				Ok(())
201			},
202
203			Err(Error) => {
204				dev_log!(
205					"extensions",
206					"warn: [CustomEditorProvider] OnSaveCustomDocument failed for '{}' at '{}': {:?}",
207					ViewType,
208					ResourceURI,
209					Error
210				);
211
212				Err(Error)
213			},
214		}
215	}
216
217	async fn ResolveCustomEditor(
218		&self,
219
220		ViewType:String,
221
222		ResourceURI:Url,
223
224		WebviewPanelHandle:String,
225	) -> Result<(), CommonError> {
226		dev_log!(
227			"extensions",
228			"[CustomEditorProvider] Resolving custom editor for '{}' on resource '{}'",
229			ViewType,
230			ResourceURI
231		);
232
233		// This is the core logic:
234		// 1. Find the sidecar that registered this ViewType. For now, assume
235		//    "cocoon-main".
236		// 2. Make an RPC call to that sidecar's implementation of
237		//    `$resolveCustomEditor`.
238		// 3. The sidecar will then call back to the host with `setHtml`, `postMessage`,
239		//    etc. to populate the webview associated with the `WebviewPanelHandle`.
240
241		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
242
243		let ResourceURIComponents = json!({ "external": ResourceURI.to_string() });
244
245		let RPCMethod = format!("{}$resolveCustomEditor", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
246
247		let RPCParameters = json!([ResourceURIComponents, ViewType, WebviewPanelHandle]);
248
249		// This is a fire-and-forget notification. The sidecar is expected to
250		// call back to the host to populate the webview.
251		IPCProvider
252			.SendNotificationToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters)
253			.await
254	}
255}