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}