Skip to main content

Mountain/Environment/
CustomEditorProvider.rs

1//! # CustomEditorProvider (Environment)
2//!
3//! RESPONSIBILITIES:
4//! - Implements
5//!   [`CustomEditorProvider`](CommonLibrary::CustomEditor::CustomEditorProvider)
6//!   for [`MountainEnvironment`]
7//! - Manages registration and lifecycle of custom non-text editors
8//! - Coordinates Webview-based editing experiences (SVG editors, diff viewers,
9//!   etc.)
10//! - Handles editor resolution, save operations, and provider unregistration
11//!
12//! ARCHITECTURAL ROLE:
13//! - Environment provider that enables extension-contributed custom editors
14//! - Uses [`IPCProvider`](CommonLibrary::IPC::IPCProvider) for RPC
15//!   communication with Cocoon
16//! - Integrates with `ApplicationState` for provider registration persistence
17//!
18//! ERROR HANDLING:
19//! - Uses [`CommonError`](CommonLibrary::Error::CommonError) for all operations
20//! - ViewType validation: rejects empty view types with InvalidArgument error
21//! - OnSaveCustomDocument now reverse-RPCs to the owning sidecar via
22//!   `$onSaveCustomDocument`; returns the sidecar's error verbatim on failure
23//!   so the workbench's save promise rejects with a real reason.
24//!
25//! PERFORMANCE:
26//! - Provider registration lookup should be O(1) via hash map in
27//!   ApplicationState (TODO)
28//! - ResolveCustomEditor uses fire-and-forget RPC pattern to avoid waiting
29//!
30//! VS CODE REFERENCE:
31//! - `vs/workbench/contrib/customEditor/browser/customEditorService.ts` -
32//!   custom editor service
33//! - `vs/workbench/contrib/customEditor/common/customEditor.ts` - custom editor
34//!   interfaces
35//! - `vs/platform/workspace/common/workspace.ts` - resource URI handling
36//!
37//! TODO:
38//! - Store provider registrations in ApplicationState with capability metadata
39//! - Implement custom editor backup/restore mechanism
40//! - Add support for multiple active instances of the same viewType
41//! - Implement custom editor move and rename handling
42//! - Add proper validation of viewType and resource URI
43//! - Implement editor-specific command registration
44//! - Add support for custom editor dispose/cleanup
45//! - Consider adding editor state persistence across reloads
46//! - Implement proper error recovery for Webview crashes
47//! - Add telemetry for custom editor usage metrics
48//!
49//! MODULE CONTENTS:
50//! - [`CustomEditorProvider`](CommonLibrary::CustomEditor::CustomEditorProvider) implementation:
51//! - `RegisterCustomEditorProvider` - register extension provider
52//! - `UnregisterCustomEditorProvider` - unregister provider
53//! - `OnSaveCustomDocument` - workbench → extension save reverse-RPC
54//! - `ResolveCustomEditor` - resolve editor content via RPC
55
56use std::sync::Arc;
57
58use CommonLibrary::{
59	CustomEditor::CustomEditorProvider::CustomEditorProvider,
60	Environment::Requires::Requires,
61	Error::CommonError::CommonError,
62	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
63};
64use async_trait::async_trait;
65use serde_json::{Value, json};
66use tauri::Emitter;
67use url::Url;
68
69use super::MountainEnvironment::MountainEnvironment;
70use crate::dev_log;
71
72#[async_trait]
73impl CustomEditorProvider for MountainEnvironment {
74	async fn RegisterCustomEditorProvider(&self, ViewType:String, _Options:Value) -> Result<(), CommonError> {
75		dev_log!(
76			"extensions",
77			"[CustomEditorProvider] Registering provider for view type: {}",
78			ViewType
79		);
80
81		// Validate ViewType is non-empty
82		if ViewType.is_empty() {
83			return Err(CommonError::InvalidArgument {
84				ArgumentName:"ViewType".to_string(),
85				Reason:"ViewType cannot be empty".to_string(),
86			});
87		}
88
89		// Register custom editor provider in ApplicationState for lifecycle management
90		// and resolution. Should associate ViewType with the sidecar identifier for
91		// RPC routing, store provider capabilities (supportsMultipleEditors,
92		// serialization support), store custom options (mime types, file extensions),
93		// validate that the ViewType is not already registered to prevent conflicts,
94		// and track registration timestamp and extension origin for debugging.
95
96		Ok(())
97	}
98
99	async fn UnregisterCustomEditorProvider(&self, ViewType:String) -> Result<(), CommonError> {
100		dev_log!(
101			"extensions",
102			"[CustomEditorProvider] Unregistering provider for view type: {}",
103			ViewType
104		);
105
106		// Remove custom editor provider registration from ApplicationState. Should
107		// check if any active editors are currently using this ViewType and either
108		// force close with unsaved changes warning or prevent unregistration, remove
109		// all stored configuration, capabilities, and sidecar association, notify the
110		// sidecar extension to clean up its internal state, and remove any cached
111		// resolution entries for this ViewType.
112
113		Ok(())
114	}
115
116	async fn OnSaveCustomDocument(&self, ViewType:String, ResourceURI:Url) -> Result<(), CommonError> {
117		dev_log!(
118			"extensions",
119			"[CustomEditorProvider] OnSaveCustomDocument called for '{}' at '{}'",
120			ViewType,
121			ResourceURI
122		);
123
124		// Workbench → extension save reverse-RPC. Cocoon's
125		// `NotificationHandler.ts:781-810` already routes
126		// `$onSaveCustomDocument` to the `customEditor.saveDocument`
127		// emitter channel which fans out to whichever provider Cocoon's
128		// `WindowNamespace.ts:188+` subscribed via `Subscribe(...)` at
129		// `registerCustomEditorProvider` time. The extension's
130		// `saveCustomDocument(document, cancellationToken)` callback
131		// runs inside Cocoon - retrieves the edited content from the
132		// webview, returns a `Thenable<void>` once the file has been
133		// written. Mountain doesn't need to write the bytes itself; the
134		// extension does that via its existing `vscode.workspace.fs`
135		// shim which Cocoon already routes back into Mountain's
136		// `FileSystem.WriteFile` IPC.
137		//
138		// Wire shape mirrors VS Code's
139		// `vs/workbench/api/common/extHostCustom.ts::ExtHostCustomEditors`
140		// `$onSaveCustomDocument` handler which expects positional args
141		// `[CustomDocumentIdentifier, CancellationTokenId]`. Mountain
142		// sends the resource URI as the document identifier (extension
143		// stored the document under this key when it returned its
144		// `CustomDocument` from `openCustomDocument`); the cancellation
145		// token id is unused by our shim path and we send `0`.
146		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
147		let DocumentIdentifier = json!({
148			"viewType": ViewType,
149			"resource": { "external": ResourceURI.to_string() },
150		});
151		let RPCMethod = format!("{}$onSaveCustomDocument", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
152		let RPCParameters = json!([DocumentIdentifier, 0]);
153		match IPCProvider
154			.SendRequestToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters, 30_000)
155			.await
156		{
157			Ok(_) => {
158				dev_log!(
159					"extensions",
160					"[CustomEditorProvider] OnSaveCustomDocument completed for '{}' at '{}'",
161					ViewType,
162					ResourceURI
163				);
164				let _ = self.ApplicationHandle.emit(
165					"sky://customEditor/saved",
166					json!({
167						"viewType": ViewType,
168						"resource": ResourceURI.to_string(),
169					}),
170				);
171				Ok(())
172			},
173			Err(Error) => {
174				dev_log!(
175					"extensions",
176					"warn: [CustomEditorProvider] OnSaveCustomDocument failed for '{}' at '{}': {:?}",
177					ViewType,
178					ResourceURI,
179					Error
180				);
181				Err(Error)
182			},
183		}
184	}
185
186	async fn ResolveCustomEditor(
187		&self,
188		ViewType:String,
189		ResourceURI:Url,
190		WebviewPanelHandle:String,
191	) -> Result<(), CommonError> {
192		dev_log!(
193			"extensions",
194			"[CustomEditorProvider] Resolving custom editor for '{}' on resource '{}'",
195			ViewType,
196			ResourceURI
197		);
198
199		// This is the core logic:
200		// 1. Find the sidecar that registered this ViewType. For now, assume
201		//    "cocoon-main".
202		// 2. Make an RPC call to that sidecar's implementation of
203		//    `$resolveCustomEditor`.
204		// 3. The sidecar will then call back to the host with `setHtml`, `postMessage`,
205		//    etc. to populate the webview associated with the `WebviewPanelHandle`.
206
207		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
208		let ResourceURIComponents = json!({ "external": ResourceURI.to_string() });
209		let RPCMethod = format!("{}$resolveCustomEditor", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
210		let RPCParameters = json!([ResourceURIComponents, ViewType, WebviewPanelHandle]);
211
212		// This is a fire-and-forget notification. The sidecar is expected to
213		// call back to the host to populate the webview.
214		IPCProvider
215			.SendNotificationToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters)
216			.await
217	}
218}