Skip to main content

Mountain/Environment/DocumentProvider/
SaveOperations.rs

1//! Document save operations.
2//!
3//! Implements `SaveDocument`, `SaveDocumentAs`, and `SaveAllDocuments` for
4//! `MountainEnvironment`.
5//!
6//! ## `save_document`
7//!
8//! Looks up the document in `OpenDocuments`, clears `IsDirty`, writes the
9//! current text to disk via `ApplicationRunTime`, emits
10//! `sky://documents/saved` to the Sky workbench, and sends
11//! `$acceptModelSaved` to Cocoon. Returns `Err` if the URI is not
12//! in `OpenDocuments` or cannot be converted to a file path.
13//!
14//! ## `save_document_as`
15//!
16//! If `new_target_uri` is `None`, shows a native save dialog via
17//! `ShowSaveDialog`. On confirmation, writes the original document's text
18//! to the new path, re-keys `OpenDocuments` from the old URI to the new one,
19//! sends `$acceptModelRemoved` + `$acceptModelAdded` to Cocoon, and emits
20//! `sky://documents/renamed`.
21//!
22//! ## `save_all_documents`
23//!
24//! Collects all dirty `file://` documents (plus untitled ones when
25//! `include_untitled` is `true`), calls `save_document` for each, and
26//! returns a `Vec<bool>` result per URI. Individual save failures are
27//! logged but do not abort the remaining saves.
28
29use std::{path::PathBuf, sync::Arc};
30
31use CommonLibrary::{
32	Effect::ApplicationRunTime::ApplicationRunTime as _,
33	Error::CommonError::CommonError,
34	FileSystem::WriteFileBytes::WriteFileBytes,
35	IPC::SkyEvent::SkyEvent,
36	UserInterface::{DTO::SaveDialogOptionsDTO::SaveDialogOptionsDTO, ShowSaveDialog::ShowSaveDialog},
37};
38use serde_json::json;
39use tauri::{Emitter, Manager};
40use url::Url;
41
42use crate::{
43	ApplicationState::DTO::DocumentStateDTO::DocumentStateDTO,
44	Environment::Utility,
45	RunTime::ApplicationRunTime::ApplicationRunTime,
46	dev_log,
47};
48
49/// Saves the document at the given URI.
50pub(super) async fn save_document(
51	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
52
53	uri:Url,
54) -> Result<bool, CommonError> {
55	dev_log!("model", "[DocumentProvider] Saving document: {}", uri);
56
57	let (content_bytes, file_path) = {
58		let mut open_documents_guard = environment
59			.ApplicationState
60			.Feature
61			.Documents
62			.OpenDocuments
63			.lock()
64			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
65
66		if let Some(document) = open_documents_guard.get_mut(uri.as_str()) {
67			// For non-file URIs, use temporary file location
68			if uri.scheme() != "file" {
69				dev_log!(
70					"model",
71					"[DocumentProvider] Saving non-file URI '{}' to temporary location",
72					uri
73				);
74			}
75
76			document.IsDirty = false;
77
78			(
79				document.GetText().into_bytes(),
80				uri.to_file_path().map_err(|_| {
81					CommonError::InvalidArgument {
82						ArgumentName:"URI".into(),
83						Reason:"Cannot convert file URI to path".into(),
84					}
85				})?,
86			)
87		} else {
88			return Err(CommonError::FileSystemNotFound(uri.to_file_path().unwrap_or_default()));
89		}
90	};
91
92	let runtime = environment.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
93
94	runtime.Run(WriteFileBytes(file_path, content_bytes, true, true)).await?;
95
96	if let Err(error) = environment
97		.ApplicationHandle
98		.emit(SkyEvent::DocumentsSaved.AsStr(), json!({ "uri": uri.to_string() }))
99	{
100		dev_log!(
101			"model",
102			"error: [DocumentProvider] Failed to emit document saved event: {}",
103			error
104		);
105	}
106
107	crate::Environment::DocumentProvider::Notifications::notify_model_saved(environment, &uri).await;
108
109	Ok(true)
110}
111
112/// Saves a document to a new location.
113pub(super) async fn save_document_as(
114	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
115
116	original_uri:Url,
117
118	new_target_uri:Option<Url>,
119) -> Result<Option<Url>, CommonError> {
120	dev_log!("model", "[DocumentProvider] Saving document as: {}", original_uri);
121
122	let runtime = environment.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
123
124	let new_file_path = match new_target_uri {
125		Some(uri) => uri.to_file_path().ok(),
126
127		None => runtime.Run(ShowSaveDialog(Some(SaveDialogOptionsDTO::default()))).await?,
128	};
129
130	let Some(new_path) = new_file_path else { return Ok(None) };
131
132	let new_uri = Url::from_file_path(&new_path).map_err(|_| {
133		CommonError::InvalidArgument {
134			ArgumentName:"NewPath".into(),
135			Reason:"Could not convert new path to URI".into(),
136		}
137	})?;
138
139	let original_content = {
140		let guard = environment
141			.ApplicationState
142			.Feature
143			.Documents
144			.OpenDocuments
145			.lock()
146			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
147
148		guard
149			.get(original_uri.as_str())
150			.map(|doc| doc.GetText())
151			.ok_or_else(|| CommonError::FileSystemNotFound(PathBuf::from(original_uri.path())))?
152	};
153
154	runtime
155		.Run(WriteFileBytes(new_path, original_content.clone().into_bytes(), true, true))
156		.await?;
157
158	let new_document_state = {
159		let mut guard = environment
160			.ApplicationState
161			.Feature
162			.Documents
163			.OpenDocuments
164			.lock()
165			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
166
167		let old_document = guard.remove(original_uri.as_str());
168
169		let new_document =
170			DocumentStateDTO::Create(new_uri.clone(), old_document.map(|d| d.LanguageIdentifier), original_content)?;
171
172		let dto = new_document.ToDTO()?;
173
174		guard.insert(new_uri.to_string(), new_document);
175
176		dto
177	};
178
179	crate::Environment::DocumentProvider::Notifications::notify_model_removed(environment, &original_uri).await;
180
181	crate::Environment::DocumentProvider::Notifications::notify_model_added(environment, &new_document_state).await;
182
183	if let Err(error) = environment.ApplicationHandle.emit(
184		SkyEvent::DocumentsRenamed.AsStr(),
185		json!({ "oldUri": original_uri.to_string(), "newUri": new_uri.to_string() }),
186	) {
187		dev_log!(
188			"model",
189			"error: [DocumentProvider] Failed to emit document renamed event: {}",
190			error
191		);
192	}
193
194	Ok(Some(new_uri))
195}
196
197/// Saves all currently dirty documents.
198pub(super) async fn save_all_documents(
199	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
200
201	include_untitled:bool,
202) -> Result<Vec<bool>, CommonError> {
203	dev_log!(
204		"model",
205		"[DocumentProvider] SaveAllDocuments called (IncludeUntitled: {})",
206		include_untitled
207	);
208
209	let uris_to_save:Vec<Url> = {
210		let open_documents_guard = environment
211			.ApplicationState
212			.Feature
213			.Documents
214			.OpenDocuments
215			.lock()
216			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
217
218		open_documents_guard
219			.values()
220			.filter(|document| {
221				// Include documents that are dirty
222				if !document.IsDirty {
223					return false;
224				}
225
226				// Include only file-scheme documents unless IncludeUntitled is true
227				if !include_untitled && document.URI.scheme() != "file" {
228					return false;
229				}
230
231				true
232			})
233			.map(|document| document.URI.clone())
234			.collect()
235	};
236
237	let mut results = Vec::with_capacity(uris_to_save.len());
238
239	dev_log!("model", "[DocumentProvider] Saving {} dirty document(s)", uris_to_save.len());
240
241	for uri in uris_to_save {
242		let result = save_document(environment, uri.clone()).await;
243
244		match &result {
245			Ok(_) => {
246				dev_log!("model", "[DocumentProvider] Successfully saved {}", uri);
247			},
248
249			Err(error) => {
250				dev_log!("model", "error: [DocumentProvider] Failed to save {}: {}", uri, error);
251			},
252		}
253
254		results.push(result.is_ok());
255	}
256
257	Ok(results)
258}