Mountain/Environment/
DocumentProvider.rs

1// File: Mountain/Source/Environment/DocumentProvider.rs
2// Role: Implements the `DocumentProvider` trait for the `MountainEnvironment`.
3// Responsibilities:
4//   - Core logic for all document lifecycle operations (open, save, change).
5//   - Notifies `Cocoon` (extension host) and `Sky` (frontend) of these events.
6//   - Handles content resolution for both native (`file`) and custom URI
7//     schemes.
8
9//! # DocumentProvider Implementation
10//!
11//! Implements the `DocumentProvider` trait for the `MountainEnvironment`. This
12//! provider contains the core logic for all document lifecycle operations, such
13//! as opening, saving, and applying text changes, and notifying the `Cocoon`
14//! sidecar and `Sky` frontend of these events.
15
16#![allow(non_snake_case, non_camel_case_types)]
17
18use std::{path::PathBuf, sync::Arc};
19
20use Common::{
21	Document::DocumentProvider::DocumentProvider,
22	Effect::ApplicationRunTime::ApplicationRunTime as _,
23	Environment::Requires::Requires,
24	Error::CommonError::CommonError,
25	FileSystem::{ReadFile::ReadFile, WriteFileBytes::WriteFileBytes},
26	IPC::IPCProvider::IPCProvider,
27	UserInterface::{DTO::SaveDialogOptionsDTO::SaveDialogOptionsDTO, ShowSaveDialog::ShowSaveDialog},
28};
29use async_trait::async_trait;
30use log::{error, info, trace, warn};
31use serde_json::{Value, json};
32use tauri::{Emitter, Manager};
33use url::Url;
34
35use super::{MountainEnvironment::MountainEnvironment, Utility};
36use crate::{
37	ApplicationState::DTO::DocumentStateDTO::DocumentStateDTO,
38	RunTime::ApplicationRunTime::ApplicationRunTime,
39};
40
41#[async_trait]
42impl DocumentProvider for MountainEnvironment {
43	/// Opens a document. If the URI scheme is not native (`file`), it attempts
44	/// to resolve the content from a registered sidecar provider
45	/// (`TextDocumentContentProvider`).
46	async fn OpenDocument(
47		&self,
48
49		URIComponentsDTO:Value,
50
51		LanguageIdentifier:Option<String>,
52
53		Content:Option<String>,
54	) -> Result<Url, CommonError> {
55		let URI = Utility::GetURLFromURIComponentsDTO(&URIComponentsDTO)?;
56
57		info!("[DocumentProvider] Opening document: {}", URI);
58
59		// First, check if the document is already open.
60		if let Some(ExistingDocument) = self
61			.ApplicationState
62			.OpenDocuments
63			.lock()
64			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
65			.get(URI.as_str())
66		{
67			info!("[DocumentProvider] Document {} is already open.", URI);
68
69			match ExistingDocument.ToDTO() {
70				Ok(DTO) => {
71					if let Err(Error) = self.ApplicationHandle.emit("sky://documents/open", DTO) {
72						error!("[DocumentProvider] Failed to emit document open event: {}", Error);
73					}
74				},
75
76				Err(Error) => {
77					error!("[DocumentProvider] Failed to serialize existing document DTO: {}", Error);
78				},
79			}
80
81			return Ok(ExistingDocument.URI.clone());
82		}
83
84		// Resolve the content based on the URI scheme.
85		let FileContent = if let Some(c) = Content {
86			c
87		} else if URI.scheme() == "file" {
88			let FilePath = URI.to_file_path().map_err(|_| {
89				CommonError::InvalidArgument {
90					ArgumentName:"URI".into(),
91
92					Reason:"Cannot convert non-file URI to path".into(),
93				}
94			})?;
95
96			let RunTime = self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
97
98			let FileContentBytes = RunTime.Run(ReadFile(FilePath.clone())).await?;
99
100			String::from_utf8(FileContentBytes)
101				.map_err(|Error| CommonError::FileSystemIO { Path:FilePath, Description:Error.to_string() })?
102		} else {
103			// Custom scheme: attempt to resolve from a sidecar provider.
104			info!(
105				"[DocumentProvider] Non-native scheme '{}'. Attempting to resolve from sidecar.",
106				URI.scheme()
107			);
108
109			let IPCProvider:Arc<dyn IPCProvider> = self.Require();
110
111			let RpcResult = IPCProvider
112				.SendRequestToSideCar(
113					// In a multi-host world, we'd look this up
114					"cocoon-main".to_string(),
115					"$provideTextDocumentContent".to_string(),
116					json!([URIComponentsDTO]),
117					10000,
118				)
119				.await?;
120
121			RpcResult.as_str().map(String::from).ok_or_else(|| {
122				CommonError::IPCError {
123					Description:format!("Failed to get valid string content for custom URI scheme '{}'", URI.scheme()),
124				}
125			})?
126		};
127
128		// The rest of the flow is the same for all schemes.
129		let NewDocument = DocumentStateDTO::Create(URI.clone(), LanguageIdentifier, FileContent);
130
131		let DTOForNotification = NewDocument.ToDTO()?;
132
133		self.ApplicationState
134			.OpenDocuments
135			.lock()
136			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
137			.insert(URI.to_string(), NewDocument);
138
139		if let Err(Error) = self.ApplicationHandle.emit("sky://documents/open", DTOForNotification.clone()) {
140			error!("[DocumentProvider] Failed to emit document open event: {}", Error);
141		}
142
143		NotifyModelAdded(self, &DTOForNotification).await;
144
145		Ok(URI)
146	}
147
148	/// Saves the document at the given URI.
149	async fn SaveDocument(&self, URI:Url) -> Result<bool, CommonError> {
150		info!("[DocumentProvider] Saving document: {}", URI);
151
152		let (ContentBytes, FilePath) = {
153			let mut OpenDocumentsGuard = self
154				.ApplicationState
155				.OpenDocuments
156				.lock()
157				.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
158
159			if let Some(Document) = OpenDocumentsGuard.get_mut(URI.as_str()) {
160				if URI.scheme() != "file" {
161					return Err(CommonError::NotImplemented {
162						FeatureName:format!(
163							"Saving for URI scheme '{}' is not supported via this method.",
164							URI.scheme()
165						),
166					});
167				}
168
169				Document.IsDirty = false;
170
171				(
172					Document.GetText().into_bytes(),
173					URI.to_file_path().map_err(|_| {
174						CommonError::InvalidArgument {
175							ArgumentName:"URI".into(),
176
177							Reason:"Cannot convert file URI to path".into(),
178						}
179					})?,
180				)
181			} else {
182				return Err(CommonError::FileSystemNotFound(URI.to_file_path().unwrap_or_default()));
183			}
184		};
185
186		let RunTime = self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
187
188		RunTime.Run(WriteFileBytes(FilePath, ContentBytes, true, true)).await?;
189
190		if let Err(Error) = self
191			.ApplicationHandle
192			.emit("sky://documents/saved", json!({ "uri": URI.to_string() }))
193		{
194			error!("[DocumentProvider] Failed to emit document saved event: {}", Error);
195		}
196
197		NotifyModelSaved(self, &URI).await;
198
199		Ok(true)
200	}
201
202	/// Saves a document to a new location.
203	async fn SaveDocumentAs(&self, OriginalURI:Url, NewTargetURI:Option<Url>) -> Result<Option<Url>, CommonError> {
204		info!("[DocumentProvider] Saving document as: {}", OriginalURI);
205
206		let RunTime = self.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
207
208		let NewFilePath = match NewTargetURI {
209			Some(uri) => uri.to_file_path().ok(),
210
211			None => RunTime.Run(ShowSaveDialog(Some(SaveDialogOptionsDTO::default()))).await?,
212		};
213
214		let Some(NewPath) = NewFilePath else { return Ok(None) };
215
216		let NewURI = Url::from_file_path(&NewPath).map_err(|_| {
217			CommonError::InvalidArgument {
218				ArgumentName:"NewPath".into(),
219
220				Reason:"Could not convert new path to URI".into(),
221			}
222		})?;
223
224		let OriginalContent = {
225			let Guard = self
226				.ApplicationState
227				.OpenDocuments
228				.lock()
229				.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
230
231			Guard
232				.get(OriginalURI.as_str())
233				.map(|doc| doc.GetText())
234				.ok_or_else(|| CommonError::FileSystemNotFound(PathBuf::from(OriginalURI.path())))?
235		};
236
237		RunTime
238			.Run(WriteFileBytes(NewPath, OriginalContent.clone().into_bytes(), true, true))
239			.await?;
240
241		let NewDocumentState = {
242			let mut Guard = self
243				.ApplicationState
244				.OpenDocuments
245				.lock()
246				.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
247
248			let OldDocument = Guard.remove(OriginalURI.as_str());
249
250			let NewDocument =
251				DocumentStateDTO::Create(NewURI.clone(), OldDocument.map(|d| d.LanguageIdentifier), OriginalContent);
252
253			let DTO = NewDocument.ToDTO()?;
254
255			Guard.insert(NewURI.to_string(), NewDocument);
256
257			DTO
258		};
259
260		NotifyModelRemoved(self, &OriginalURI).await;
261
262		NotifyModelAdded(self, &NewDocumentState).await;
263
264		if let Err(Error) = self.ApplicationHandle.emit(
265			"sky://documents/renamed",
266			json!({ "oldUri": OriginalURI.to_string(), "newUri": NewURI.to_string() }),
267		) {
268			error!("[DocumentProvider] Failed to emit document renamed event: {}", Error);
269		}
270
271		Ok(Some(NewURI))
272	}
273
274	/// Saves all currently dirty documents.
275	async fn SaveAllDocuments(&self, _IncludeUntitled:bool) -> Result<Vec<bool>, CommonError> {
276		warn!("[DocumentProvider] SaveAllDocuments is not implemented.");
277
278		Err(CommonError::NotImplemented { FeatureName:"SaveAllDocuments".into() })
279	}
280
281	/// Applies a collection of content changes to a document.
282	async fn ApplyDocumentChanges(
283		&self,
284
285		URI:Url,
286
287		NewVersionIdentifier:i64,
288
289		ChangesDTOCollection:Value,
290
291		_IsDirtyAfterChange:bool,
292
293		_IsUndoing:bool,
294
295		_IsRedoing:bool,
296	) -> Result<(), CommonError> {
297		trace!("[DocumentProvider] Applying changes to document: {}", URI);
298
299		{
300			let mut OpenDocumentsGuard = self
301				.ApplicationState
302				.OpenDocuments
303				.lock()
304				.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
305
306			if let Some(Document) = OpenDocumentsGuard.get_mut(URI.as_str()) {
307				Document.ApplyChanges(NewVersionIdentifier, &ChangesDTOCollection)?;
308			} else {
309				warn!("[DocumentProvider] Received changes for unknown document: {}", URI);
310
311				return Ok(());
312			}
313		}
314
315		NotifyModelChanged(self, &URI, NewVersionIdentifier, ChangesDTOCollection).await;
316
317		Ok(())
318	}
319}
320
321// --- Internal Notification Helpers ---
322
323/// Notifies Cocoon that a new document model has been added.
324async fn NotifyModelAdded(Environment:&MountainEnvironment, DocumentStateDTO:&Value) {
325	let URIString = DocumentStateDTO.get("URI").and_then(Value::as_str).unwrap_or("unknown");
326
327	info!("[DocumentProvider] Notifying ModelAdded for: {}", URIString);
328
329	let Payload = json!([DocumentStateDTO]);
330
331	let IPCProvider:Arc<dyn IPCProvider> = Environment.Require();
332
333	if let Err(Error) = IPCProvider
334		.SendNotificationToSideCar("cocoon-main".to_string(), "$acceptModelAdded".to_string(), Payload)
335		.await
336	{
337		error!(
338			"[DocumentProvider] Failed to send $acceptModelAdded for {}: {}",
339			URIString, Error
340		);
341	}
342}
343
344/// Notifies Cocoon that a document's content has changed.
345async fn NotifyModelChanged(Environment:&MountainEnvironment, URI:&Url, NewVersion:i64, Changes:Value) {
346	info!("[DocumentProvider] Notifying ModelChanged for: {}", URI);
347
348	let URIComponents = json!({ "external": URI.to_string(), "$mid": 1 });
349
350	let EventData = json!({ "versionId": NewVersion, "changes": Changes, "isDirty": true });
351
352	let Payload = json!([URIComponents, EventData]);
353
354	let IPCProvider:Arc<dyn IPCProvider> = Environment.Require();
355
356	if let Err(Error) = IPCProvider
357		.SendNotificationToSideCar("cocoon-main".to_string(), "$acceptModelChanged".to_string(), Payload)
358		.await
359	{
360		error!("[DocumentProvider] Failed to send $acceptModelChanged for {}: {}", URI, Error);
361	}
362}
363
364/// Notifies Cocoon that a document has been saved to disk.
365async fn NotifyModelSaved(Environment:&MountainEnvironment, URI:&Url) {
366	info!("[DocumentProvider] Notifying ModelSaved for: {}", URI);
367
368	let URIComponents = json!({ "external": URI.to_string(), "$mid": 1 });
369
370	let Payload = json!([URIComponents]);
371
372	let IPCProvider:Arc<dyn IPCProvider> = Environment.Require();
373
374	if let Err(Error) = IPCProvider
375		.SendNotificationToSideCar("cocoon-main".to_string(), "$acceptModelSaved".to_string(), Payload)
376		.await
377	{
378		error!("[DocumentProvider] Failed to send $acceptModelSaved for {}: {}", URI, Error);
379	}
380}
381
382/// Notifies Cocoon that a document has been closed or renamed.
383pub async fn NotifyModelRemoved(Environment:&MountainEnvironment, URI:&Url) {
384	info!("[DocumentProvider] Notifying ModelRemoved for: {}", URI);
385
386	let URIComponents = json!({ "external": URI.to_string(), "$mid": 1 });
387
388	let Payload = json!([URIComponents]);
389
390	let IPCProvider:Arc<dyn IPCProvider> = Environment.Require();
391
392	if let Err(Error) = IPCProvider
393		.SendNotificationToSideCar("cocoon-main".to_string(), "$acceptModelRemoved".to_string(), Payload)
394		.await
395	{
396		error!("[DocumentProvider] Failed to send $acceptModelRemoved for {}: {}", URI, Error);
397	}
398}