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}