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