Skip to main content

Mountain/Environment/DocumentProvider/
SaveOperations.rs

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