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//! - Some operations are stubbed with logging/warning (OnSaveCustomDocument)
22//!
23//! PERFORMANCE:
24//! - Provider registration lookup should be O(1) via hash map in
25//!   ApplicationState (TODO)
26//! - ResolveCustomEditor uses fire-and-forget RPC pattern to avoid waiting
27//!
28//! VS CODE REFERENCE:
29//! - `vs/workbench/contrib/customEditor/browser/customEditorService.ts` -
30//!   custom editor service
31//! - `vs/workbench/contrib/customEditor/common/customEditor.ts` - custom editor
32//!   interfaces
33//! - `vs/platform/workspace/common/workspace.ts` - resource URI handling
34//!
35//! TODO:
36//! - Store provider registrations in ApplicationState with capability metadata
37//! - Implement custom editor backup/restore mechanism
38//! - Add support for multiple active instances of the same viewType
39//! - Implement custom editor move and rename handling
40//! - Add proper validation of viewType and resource URI
41//! - Implement editor-specific command registration
42//! - Add support for custom editor dispose/cleanup
43//! - Consider adding editor state persistence across reloads
44//! - Implement proper error recovery for Webview crashes
45//! - Add telemetry for custom editor usage metrics
46//!
47//! MODULE CONTENTS:
48//! - [`CustomEditorProvider`](CommonLibrary::CustomEditor::CustomEditorProvider) implementation:
49//! - `RegisterCustomEditorProvider` - register extension provider
50//! - `UnregisterCustomEditorProvider` - unregister provider
51//! - `OnSaveCustomDocument` - save handler (stub)
52//! - `ResolveCustomEditor` - resolve editor content via RPC
53
54use std::sync::Arc;
55
56use CommonLibrary::{
57	CustomEditor::CustomEditorProvider::CustomEditorProvider,
58	Environment::Requires::Requires,
59	Error::CommonError::CommonError,
60	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
61};
62use async_trait::async_trait;
63use serde_json::{Value, json};
64use url::Url;
65
66use super::MountainEnvironment::MountainEnvironment;
67use crate::dev_log;
68
69#[async_trait]
70impl CustomEditorProvider for MountainEnvironment {
71	async fn RegisterCustomEditorProvider(&self, ViewType:String, _Options:Value) -> Result<(), CommonError> {
72		dev_log!(
73			"extensions",
74			"[CustomEditorProvider] Registering provider for view type: {}",
75			ViewType
76		);
77
78		// Validate ViewType is non-empty
79		if ViewType.is_empty() {
80			return Err(CommonError::InvalidArgument {
81				ArgumentName:"ViewType".to_string(),
82				Reason:"ViewType cannot be empty".to_string(),
83			});
84		}
85
86		// Register custom editor provider in ApplicationState for lifecycle management
87		// and resolution. Should associate ViewType with the sidecar identifier for
88		// RPC routing, store provider capabilities (supportsMultipleEditors,
89		// serialization support), store custom options (mime types, file extensions),
90		// validate that the ViewType is not already registered to prevent conflicts,
91		// and track registration timestamp and extension origin for debugging.
92
93		Ok(())
94	}
95
96	async fn UnregisterCustomEditorProvider(&self, ViewType:String) -> Result<(), CommonError> {
97		dev_log!(
98			"extensions",
99			"[CustomEditorProvider] Unregistering provider for view type: {}",
100			ViewType
101		);
102
103		// Remove custom editor provider registration from ApplicationState. Should
104		// check if any active editors are currently using this ViewType and either
105		// force close with unsaved changes warning or prevent unregistration, remove
106		// all stored configuration, capabilities, and sidecar association, notify the
107		// sidecar extension to clean up its internal state, and remove any cached
108		// resolution entries for this ViewType.
109
110		Ok(())
111	}
112
113	async fn OnSaveCustomDocument(&self, ViewType:String, ResourceURI:Url) -> Result<(), CommonError> {
114		dev_log!(
115			"extensions",
116			"[CustomEditorProvider] OnSaveCustomDocument called for '{}' at '{}'",
117			ViewType,
118			ResourceURI
119		);
120
121		// Implement the complete custom document save workflow. Send RPC request
122		// ($customDocument/save) to the extension sidecar responsible for this
123		// ViewType, including the ResourceURI. The extension retrieves edited content
124		// from its Webview via postMessage and returns the updated data (string or
125		// bytes). Mountain receives the content and writes it to the file system using
126		// FileSystemWriter with appropriate encoding and atomic write pattern. Emit
127		// a document saved notification to refresh UI and trigger post-save hooks
128		// (formatters, linters, extension notifications). This enables custom editors
129		// to participate in the standard save lifecycle.
130
131		dev_log!(
132			"extensions",
133			"warn: [CustomEditorProvider] OnSaveCustomDocument is not fully implemented."
134		);
135		Ok(())
136	}
137
138	async fn ResolveCustomEditor(
139		&self,
140		ViewType:String,
141		ResourceURI:Url,
142		WebviewPanelHandle:String,
143	) -> Result<(), CommonError> {
144		dev_log!(
145			"extensions",
146			"[CustomEditorProvider] Resolving custom editor for '{}' on resource '{}'",
147			ViewType,
148			ResourceURI
149		);
150
151		// This is the core logic:
152		// 1. Find the sidecar that registered this ViewType. For now, assume
153		//    "cocoon-main".
154		// 2. Make an RPC call to that sidecar's implementation of
155		//    `$resolveCustomEditor`.
156		// 3. The sidecar will then call back to the host with `setHtml`, `postMessage`,
157		//    etc. to populate the webview associated with the `WebviewPanelHandle`.
158
159		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
160		let ResourceURIComponents = json!({ "external": ResourceURI.to_string() });
161		let RPCMethod = format!("{}$resolveCustomEditor", ProxyTarget::ExtHostCustomEditors.GetTargetPrefix());
162		let RPCParameters = json!([ResourceURIComponents, ViewType, WebviewPanelHandle]);
163
164		// This is a fire-and-forget notification. The sidecar is expected to
165		// call back to the host to populate the webview.
166		IPCProvider
167			.SendNotificationToSideCar("cocoon-main".to_string(), RPCMethod, RPCParameters)
168			.await
169	}
170}