Mountain/Environment/
WorkSpaceProvider.rs

1// File: Mountain/Source/Environment/WorkSpaceProvider.rs
2// Role: Implements `WorkSpaceProvider` and `WorkSpaceEditApplier` traits.
3// Responsibilities:
4//   - Core logic for workspace-related operations.
5//   - Querying workspace folders, finding files, and applying workspace edits.
6//   - Orchestrating the opening of files, including routing to custom editors.
7
8//! # WorkSpaceProvider Implementation
9//!
10//! Implements the `WorkSpaceProvider` and `WorkSpaceEditApplier` traits for
11//! the `MountainEnvironment`. This provider contains the core logic for
12//! workspace-related operations, including querying workspace folders and
13//! performing workspace-wide file searches.
14
15#![allow(non_snake_case, non_camel_case_types)]
16
17use std::{path::PathBuf, sync::Arc};
18
19use Common::{
20	CustomEditor::CustomEditorProvider::CustomEditorProvider,
21	DTO::WorkSpaceEditDTO::WorkSpaceEditDTO,
22	Document::DocumentProvider::DocumentProvider,
23	Environment::Requires::Requires,
24	Error::CommonError::CommonError,
25	WebView::WebViewProvider::WebViewProvider,
26	WorkSpace::{WorkSpaceEditApplier::WorkSpaceEditApplier, WorkSpaceProvider::WorkSpaceProvider},
27};
28use async_trait::async_trait;
29use globset::{Glob, GlobMatcher};
30use ignore::WalkBuilder;
31use log::{info, warn};
32use serde_json::{Value, json};
33use url::Url;
34
35use super::{MountainEnvironment::MountainEnvironment, Utility};
36
37#[async_trait]
38impl WorkSpaceProvider for MountainEnvironment {
39	/// Retrieves information about all currently open workspace folders.
40	async fn GetWorkSpaceFoldersInfo(&self) -> Result<Vec<(Url, String, usize)>, CommonError> {
41		info!("[WorkSpaceProvider] Getting workspace folders info.");
42		let FoldersGuard = self
43			.ApplicationState
44			.WorkSpaceFolders
45			.lock()
46			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
47		Ok(FoldersGuard.iter().map(|f| (f.URI.clone(), f.Name.clone(), f.Index)).collect())
48	}
49
50	/// Retrieves information for the specific workspace folder that contains a
51	/// given URI.
52	async fn GetWorkSpaceFolderInfo(&self, URIToMatch:Url) -> Result<Option<(Url, String, usize)>, CommonError> {
53		let FoldersGuard = self
54			.ApplicationState
55			.WorkSpaceFolders
56			.lock()
57			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
58		for Folder in FoldersGuard.iter() {
59			if URIToMatch.as_str().starts_with(Folder.URI.as_str()) {
60				return Ok(Some((Folder.URI.clone(), Folder.Name.clone(), Folder.Index)));
61			}
62		}
63
64		Ok(None)
65	}
66
67	/// Gets the name of the current workspace.
68	async fn GetWorkSpaceName(&self) -> Result<Option<String>, CommonError> {
69		self.ApplicationState.GetWorkSpaceIdentifier().map(Some)
70	}
71
72	/// Gets the path to the workspace configuration file (`.code-workspace`).
73	async fn GetWorkSpaceConfigurationPath(&self) -> Result<Option<PathBuf>, CommonError> {
74		Ok(self
75			.ApplicationState
76			.WorkSpaceConfigurationPath
77			.lock()
78			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
79			.clone())
80	}
81
82	/// Checks if the current workspace is trusted.
83	async fn IsWorkSpaceTrusted(&self) -> Result<bool, CommonError> {
84		Ok(self.ApplicationState.IsTrusted.load(std::sync::atomic::Ordering::Relaxed))
85	}
86
87	/// Requests workspace trust from the user.
88	async fn RequestWorkSpaceTrust(&self, _Options:Option<Value>) -> Result<bool, CommonError> {
89		warn!("[WorkSpaceProvider] RequestWorkSpaceTrust is not implemented; defaulting to trusted.");
90		// A full implementation would show a modal dialog to the user and wait for
91		// their response.
92		self.ApplicationState
93			.IsTrusted
94			.store(true, std::sync::atomic::Ordering::Relaxed);
95		Ok(true)
96	}
97
98	/// Finds files within the workspace using glob patterns.
99	async fn FindFilesInWorkSpace(
100		&self,
101
102		IncludePatternDTO:Value,
103
104		ExcludePatternDTO:Option<Value>,
105
106		MaxResults:Option<usize>,
107
108		UseIgnoreFiles:bool,
109
110		FollowSymlinks:bool,
111	) -> Result<Vec<Url>, CommonError> {
112		info!(
113			"[WorkSpaceProvider] Finding files with include pattern: {:?}",
114			IncludePatternDTO
115		);
116		let FoldersGuard = self
117			.ApplicationState
118			.WorkSpaceFolders
119			.lock()
120			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
121		if FoldersGuard.is_empty() {
122			return Ok(vec![]);
123		}
124
125		let IncludeMatcher = BuildGlobMatcher(IncludePatternDTO)?;
126		let ExcludeMatcher = ExcludePatternDTO.map(BuildGlobMatcher).transpose()?.flatten();
127		let mut Results:Vec<Url> = Vec::new();
128		let MaxResultsCap = MaxResults.unwrap_or(usize::MAX);
129
130		for Folder in FoldersGuard.iter() {
131			if Results.len() >= MaxResultsCap {
132				break;
133			}
134
135			let FolderPath = match Folder.URI.to_file_path() {
136				Ok(path) => path,
137
138				Err(_) => continue,
139			};
140			let mut WalkerBuilder = WalkBuilder::new(&FolderPath);
141			WalkerBuilder.standard_filters(UseIgnoreFiles).follow_links(FollowSymlinks);
142
143			for EntryResult in WalkerBuilder.build() {
144				if Results.len() >= MaxResultsCap {
145					break;
146				}
147
148				if let Ok(Entry) = EntryResult {
149					let Path = Entry.path();
150					if Path.is_dir() {
151						continue;
152					}
153
154					if !IncludeMatcher.as_ref().map_or(true, |g| g.is_match(Path)) {
155						continue;
156					}
157
158					if let Some(ref exclude) = ExcludeMatcher {
159						if exclude.is_match(Path) {
160							continue;
161						}
162					}
163
164					if let Ok(URL) = Url::from_file_path(Path) {
165						Results.push(URL);
166					}
167				}
168			}
169		}
170
171		Ok(Results)
172	}
173
174	/// Requests that the host application open the specified file path in an
175	/// editor.
176	async fn OpenFile(&self, Path:PathBuf) -> Result<(), CommonError> {
177		let URI = Url::from_file_path(Path.clone()).map_err(|_| {
178			CommonError::InvalidArgument { ArgumentName:"Path".into(), Reason:"Could not convert path to URI.".into() }
179		})?;
180
181		// A full implementation would check a registry of custom editor providers based
182		// on the file's glob pattern.
183		let CustomEditorViewType:Option<String> = None;
184
185		if let Some(ViewType) = CustomEditorViewType {
186			info!(
187				"[WorkSpaceProvider] Found custom editor '{}' for file '{}'",
188				ViewType,
189				Path.display()
190			);
191			let WebViewProvider:Arc<dyn WebViewProvider> = self.Require();
192			let Handle = WebViewProvider
193				.CreateWebViewPanel(
194					json!({ "id": "placeholder.extension" }),
195					ViewType.clone(),
196					Path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default(),
197					json!({ "viewColumn": -1 }),
198					json!({}),
199					json!({ "enableScripts": true }),
200				)
201				.await?;
202			let CustomEditorProvider:Arc<dyn CustomEditorProvider> = self.Require();
203			CustomEditorProvider.ResolveCustomEditor(ViewType, URI, Handle).await?;
204			return Ok(());
205		}
206
207		info!(
208			"[WorkSpaceProvider] No custom editor found. Opening '{}' as text.",
209			Path.display()
210		);
211		let URIComponents = json!({ "external": URI.to_string(), "$mid": 1 });
212		let DocProvider:Arc<dyn DocumentProvider> = self.Require();
213		DocProvider.OpenDocument(URIComponents, None, None).await?;
214		Ok(())
215	}
216}
217
218fn BuildGlobMatcher(GlobValue:Value) -> Result<Option<GlobMatcher>, CommonError> {
219	GlobValue
220		.as_str()
221		.map(|Pattern| {
222			Glob::new(Pattern).map(|g| g.compile_matcher()).map_err(|Error| {
223				CommonError::InvalidArgument { ArgumentName:"GlobPattern".to_string(), Reason:Error.to_string() }
224			})
225		})
226		.transpose()
227}
228
229#[async_trait]
230impl WorkSpaceEditApplier for MountainEnvironment {
231	async fn ApplyWorkSpaceEdit(&self, EditDTO:WorkSpaceEditDTO) -> Result<bool, CommonError> {
232		let DocProvider:Arc<dyn DocumentProvider> = self.Require();
233		for (URIValue, Edits) in EditDTO.Edits {
234			let URI = serde_json::from_value::<Url>(URIValue)?;
235			let Document = {
236				self.ApplicationState
237					.OpenDocuments
238					.lock()
239					.map_err(Utility::MapApplicationStateLockErrorToCommonError)?
240					.get(URI.as_str())
241					.cloned()
242			};
243
244			if let Some(Doc) = Document {
245				let NewVersionID = Doc.Version + 1;
246				DocProvider
247					.ApplyDocumentChanges(URI.clone(), NewVersionID, json!(Edits), true, false, false)
248					.await?;
249			} else {
250				warn!("[WorkSpaceProvider] Attempted to apply edit to non-open document: {}", URI);
251			}
252		}
253
254		Ok(true)
255	}
256}