Skip to main content

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::sync::Arc;
28
29use CommonLibrary::{
30	CustomEditor::CustomEditorProvider::CustomEditorProvider,
31	Environment::Requires::Requires,
32	Error::CommonError::CommonError,
33	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
34};
35use async_trait::async_trait;
36use serde_json::{Value, json};
37use tauri::Emitter;
38use url::Url;
39
40use super::MountainEnvironment::MountainEnvironment;
41use crate::dev_log;
42
43#[async_trait]
44impl CustomEditorProvider for MountainEnvironment {
45	async fn RegisterCustomEditorProvider(&self, ViewType:String, _Options:Value) -> Result<(), CommonError> {
46		dev_log!(
47			"extensions",
48			"[CustomEditorProvider] Registering provider for view type: {}",
49			ViewType
50		);
51
52		// Validate ViewType is non-empty
53		if ViewType.is_empty() {
54			return Err(CommonError::InvalidArgument {
55				ArgumentName:"ViewType".to_string(),
56				Reason:"ViewType cannot be empty".to_string(),
57			});
58		}
59
60		// TODO: Store in ApplicationState associating ViewType with the sidecar
61		// identifier for RPC routing, record provider capabilities
62		// (supportsMultipleEditors, serialization), validate no duplicate
63		// ViewType, and track registration timestamp and extension origin.
64
65		Ok(())
66	}
67
68	async fn UnregisterCustomEditorProvider(&self, ViewType:String) -> Result<(), CommonError> {
69		dev_log!(
70			"extensions",
71			"[CustomEditorProvider] Unregistering provider for view type: {}",
72			ViewType
73		);
74
75		// TODO: Check for active editors using this ViewType, force close or
76		// block, remove config/capabilities/sidecar association, notify sidecar
77		// to clean up, and remove cached resolution entries.
78
79		Ok(())
80	}
81
82	async fn OnSaveCustomDocument(&self, ViewType:String, ResourceURI:Url) -> Result<(), CommonError> {
83		dev_log!(
84			"extensions",
85			"[CustomEditorProvider] OnSaveCustomDocument called for '{}' at '{}'",
86			ViewType,
87			ResourceURI
88		);
89
90		// Workbench → extension save reverse-RPC. Cocoon's
91		// `NotificationHandler.ts:781-810` already routes
92		// `$onSaveCustomDocument` to the `customEditor.saveDocument`
93		// emitter channel which fans out to whichever provider Cocoon's
94		// `WindowNamespace.ts:188+` subscribed via `Subscribe(...)` at
95		// `registerCustomEditorProvider` time. The extension's
96		// `saveCustomDocument(document, cancellationToken)` callback
97		// runs inside Cocoon - retrieves the edited content from the
98		// webview, returns a `Thenable<void>` once the file has been
99		// written. Mountain doesn't need to write the bytes itself; the
100		// extension does that via its existing `vscode.workspace.fs`
101		// shim which Cocoon already routes back into Mountain's
102		// `FileSystem.WriteFile` IPC.
103		//
104		// Wire shape mirrors VS Code's
105		// `vs/workbench/api/common/extHostCustom.ts::ExtHostCustomEditors`
106		// `$onSaveCustomDocument` handler which expects positional args
107		// `[CustomDocumentIdentifier, CancellationTokenId]`. Mountain
108		// sends the resource URI as the document identifier (extension
109		// stored the document under this key when it returned its
110		// `CustomDocument` from `openCustomDocument`); the cancellation
111		// token id is unused by our shim path and we send `0`.
112		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
113
114		let DocumentIdentifier = json!({
115			"viewType": ViewType,
116			"resource": { "external": ResourceURI.to_string() },
117		});
118
119		let RPCMethod = format!("{}$onSaveCustomDocument", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
120
121		let RPCParameters = json!([DocumentIdentifier, 0]);
122
123		match IPCProvider
124			.SendRequestToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters, 30_000)
125			.await
126		{
127			Ok(_) => {
128				dev_log!(
129					"extensions",
130					"[CustomEditorProvider] OnSaveCustomDocument completed for '{}' at '{}'",
131					ViewType,
132					ResourceURI
133				);
134
135				let _ = self.ApplicationHandle.emit(
136					"sky://customEditor/saved",
137					json!({
138						"viewType": ViewType,
139						"resource": ResourceURI.to_string(),
140					}),
141				);
142
143				Ok(())
144			},
145
146			Err(Error) => {
147				dev_log!(
148					"extensions",
149					"warn: [CustomEditorProvider] OnSaveCustomDocument failed for '{}' at '{}': {:?}",
150					ViewType,
151					ResourceURI,
152					Error
153				);
154
155				Err(Error)
156			},
157		}
158	}
159
160	async fn ResolveCustomEditor(
161		&self,
162
163		ViewType:String,
164
165		ResourceURI:Url,
166
167		WebviewPanelHandle:String,
168	) -> Result<(), CommonError> {
169		dev_log!(
170			"extensions",
171			"[CustomEditorProvider] Resolving custom editor for '{}' on resource '{}'",
172			ViewType,
173			ResourceURI
174		);
175
176		// This is the core logic:
177		// 1. Find the sidecar that registered this ViewType. For now, assume
178		//    "cocoon-main".
179		// 2. Make an RPC call to that sidecar's implementation of
180		//    `$resolveCustomEditor`.
181		// 3. The sidecar will then call back to the host with `setHtml`, `postMessage`,
182		//    etc. to populate the webview associated with the `WebviewPanelHandle`.
183
184		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
185
186		let ResourceURIComponents = json!({ "external": ResourceURI.to_string() });
187
188		let RPCMethod = format!("{}$resolveCustomEditor", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
189
190		let RPCParameters = json!([ResourceURIComponents, ViewType, WebviewPanelHandle]);
191
192		// This is a fire-and-forget notification. The sidecar is expected to
193		// call back to the host to populate the webview.
194		IPCProvider
195			.SendNotificationToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters)
196			.await
197	}
198}