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