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