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}