Mountain/ExtensionManagement/
Scanner.rs

1//! # Extension Scanner (ExtensionManagement)
2//!
3//! Contains the logic for scanning directories on the filesystem to discover
4//! installed extensions by reading their `package.json` manifests, and for
5//! collecting default configuration values from all discovered extensions.
6//!
7//! ## RESPONSIBILITIES
8//!
9//! ### 1. Extension Discovery
10//! - Scan registered extension paths for valid extensions
11//! - Read and parse `package.json` manifest files
12//! - Validate extension metadata and structure
13//! - Build `ExtensionDescriptionStateDTO` for each discovered extension
14//!
15//! ### 2. Configuration Collection
16//! - Extract default configuration values from extension
17//!   `contributes.configuration`
18//! - Merge configuration properties from all extensions
19//! - Handle nested configuration objects recursively
20//! - Detect and prevent circular references
21//!
22//! ### 3. Error Handling
23//! - Gracefully handle unreadable directories
24//! - Skip extensions with invalid package.json
25//! - Log warnings for partial scan failures
26//! - Continue scanning even when some paths fail
27//!
28//! ## ARCHITECTURAL ROLE
29//!
30//! The Extension Scanner is part of the **Extension Management** subsystem:
31//!
32//! ```text
33//! Startup ──► ScanPaths ──► Scanner ──► Extensions Map ──► ApplicationState
34//! ```
35//!
36//! ### Position in Mountain
37//! - `ExtensionManagement` module: Extension discovery and metadata
38//! - Used during application startup to populate extension registry
39//! - Provides data to `Cocoon` for extension host initialization
40//!
41//! ### Dependencies
42//! - `CommonLibrary::FileSystem`: ReadDirectory and ReadFile effects
43//! - `CommonLibrary::Error::CommonError`: Error handling
44//! - `ApplicationRunTime`: Effect execution
45//! - `ApplicationState`: Extension storage
46//!
47//! ### Dependents
48//! - `InitializationData::ConstructExtensionHostInitializationData`: Sends
49//!   extensions to Cocoon
50//! - `MountainEnvironment::ScanForExtensions`: Public API for extension
51//!   scanning
52//! - `ApplicationState::Internal::ScanExtensionsWithRecovery`: Robust scanning
53//!   wrapper
54//!
55//! ## SCANNING PROCESS
56//!
57//! 1. **Path Resolution**: Get scan paths from
58//!    `ApplicationState.Extension.Registry.ExtensionScanPaths`
59//! 2. **Directory Enumeration**: For each path, read directory entries
60//! 3. **Manifest Detection**: Look for `package.json` in each subdirectory
61//! 4. **Parsing**: Deserialize `package.json` into
62//!    `ExtensionDescriptionStateDTO`
63//! 5. **Augmentation**: Add `ExtensionLocation` (disk path) to metadata
64//! 6. **Storage**: Insert into `ApplicationState.Extension.ScannedExtensions`
65//!    map
66//!
67//! ## CONFIGURATION MERGING
68//!
69//! `CollectDefaultConfigurations()` extracts default values from all
70//! extensions' `contributes.configuration.properties` and merges them into a
71//! single JSON object:
72//!
73//! - Handles nested `.` notation (e.g., `editor.fontSize`)
74//! - Recursively processes nested `properties` objects
75//! - Detects circular references to prevent infinite loops
76//! - Returns a flat map of configuration keys to default values
77//!
78//! ## ERROR HANDLING
79//!
80//! - **Directory Read Failures**: Logged as warnings, scanning continues
81//! - **Invalid package.json**: Skipped with warning, scanning continues
82//! - **IO Errors**: Logged, operation continues or fails gracefully
83//!
84//! ## PERFORMANCE
85//!
86//! - Scans are performed asynchronously via `ApplicationRunTime`
87//! - Each directory read is a separate filesystem operation
88//! - Large extension directories may impact startup time
89//! - Consider caching scan results for development workflows
90//!
91//! ## VS CODE REFERENCE
92//!
93//! Borrowed from VS Code's extension management:
94//! - `vs/workbench/services/extensions/common/extensionPoints.ts` -
95//!   Configuration contribution
96//! - `vs/platform/extensionManagement/common/extensionManagementService.ts` -
97//!   Extension scanning
98//!
99//! ## TODO
100//!
101//! - [ ] Implement concurrent scanning for multiple paths
102//! - [ ] Add extension scan caching with invalidation
103//! - [ ] Implement extension validation rules (required fields, etc.)
104//! - [ ] Add scan progress reporting for UI feedback
105//! - [ ] Support extension scanning in subdirectories (recursive)
106//!
107//! ## MODULE CONTENTS
108//!
109//! - [`ScanDirectoryForExtensions`]: Scan a single directory for extensions
110//! - [`CollectDefaultConfigurations`]: Merge configuration defaults from all
111//!   extensions
112//! - `process_configuration_properties`: Recursive configuration property
113//! processor
114
115use std::{path::PathBuf, sync::Arc};
116
117use CommonLibrary::{
118	Effect::ApplicationRunTime::ApplicationRunTime as _,
119	Error::CommonError::CommonError,
120	FileSystem::{DTO::FileTypeDTO::FileTypeDTO, ReadDirectory::ReadDirectory, ReadFile::ReadFile},
121};
122use log::{trace, warn};
123use serde_json::{Map, Value};
124use tauri::Manager;
125
126use crate::{
127	ApplicationState::{ApplicationState, DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO},
128	Environment::Utility,
129	RunTime::ApplicationRunTime::ApplicationRunTime,
130};
131
132/// Scans a single directory for valid extensions.
133///
134/// This function iterates through a given directory, looking for subdirectories
135/// that contain a `package.json` file. It then attempts to parse this file
136/// into an `ExtensionDescriptionStateDTO`.
137pub async fn ScanDirectoryForExtensions(
138	ApplicationHandle:tauri::AppHandle,
139
140	DirectoryPath:PathBuf,
141) -> Result<Vec<ExtensionDescriptionStateDTO>, CommonError> {
142	let RunTime = ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
143
144	let mut FoundExtensions = Vec::new();
145
146	let TopLevelEntries = match RunTime.Run(ReadDirectory(DirectoryPath.clone())).await {
147		Ok(entries) => entries,
148
149		Err(error) => {
150			warn!(
151				"[ExtensionScanner] Could not read extension directory '{}': {}. Skipping.",
152				DirectoryPath.display(),
153				error
154			);
155
156			return Ok(Vec::new());
157		},
158	};
159
160	for (EntryName, FileType) in TopLevelEntries {
161		if FileType == FileTypeDTO::Directory {
162			let PotentialExtensionPath = DirectoryPath.join(EntryName);
163
164			let PackageJsonPath = PotentialExtensionPath.join("package.json");
165
166			trace!(
167				"[ExtensionScanner] Checking for package.json in: {}",
168				PotentialExtensionPath.display()
169			);
170
171			if let Ok(PackageJsonContent) = RunTime.Run(ReadFile(PackageJsonPath)).await {
172				match serde_json::from_slice::<ExtensionDescriptionStateDTO>(&PackageJsonContent) {
173					Ok(mut Description) => {
174						// Augment the description with its location on disk.
175						Description.ExtensionLocation =
176							serde_json::to_value(url::Url::from_directory_path(PotentialExtensionPath).unwrap())
177								.unwrap_or(Value::Null);
178
179						FoundExtensions.push(Description);
180					},
181
182					Err(error) => {
183						warn!(
184							"[ExtensionScanner] Failed to parse package.json for extension at '{}': {}",
185							PotentialExtensionPath.display(),
186							error
187						);
188					},
189				}
190			}
191		}
192	}
193
194	Ok(FoundExtensions)
195}
196
197/// A helper function to extract default configuration values from all
198/// scanned extensions.
199pub fn CollectDefaultConfigurations(State:&ApplicationState) -> Result<Value, CommonError> {
200	let mut MergedDefaults = Map::new();
201
202	let Extensions = State
203		.Extension
204		.ScannedExtensions
205		.ScannedExtensions
206		.lock()
207		.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
208
209	for Extension in Extensions.values() {
210		if let Some(contributes) = Extension.Contributes.as_ref().and_then(|v| v.as_object()) {
211			if let Some(configuration) = contributes.get("configuration").and_then(|v| v.as_object()) {
212				if let Some(properties) = configuration.get("properties").and_then(|v| v.as_object()) {
213					// NESTED OBJECT HANDLING: Recursively process configuration properties
214					self::process_configuration_properties(&mut MergedDefaults, "", properties, &mut Vec::new())?;
215				}
216			}
217		}
218	}
219
220	Ok(Value::Object(MergedDefaults))
221}
222
223/// RECURSIVE CONFIGURATION PROCESSING: Handle nested object structures
224fn process_configuration_properties(
225	merged_defaults:&mut serde_json::Map<String, Value>,
226	current_path:&str,
227	properties:&serde_json::Map<String, Value>,
228	visited_keys:&mut Vec<String>,
229) -> Result<(), CommonError> {
230	for (key, value) in properties {
231		// Build the full path for this property
232		let full_path = if current_path.is_empty() {
233			key.clone()
234		} else {
235			format!("{}.{}", current_path, key)
236		};
237
238		// Check for circular references
239		if visited_keys.contains(&full_path) {
240			return Err(CommonError::Unknown {
241				Description:format!("Circular reference detected in configuration properties: {}", full_path),
242			});
243		}
244
245		visited_keys.push(full_path.clone());
246
247		if let Some(prop_details) = value.as_object() {
248			// Check if this is a nested object structure
249			if let Some(nested_properties) = prop_details.get("properties").and_then(|v| v.as_object()) {
250				// Recursively process nested properties
251				self::process_configuration_properties(merged_defaults, &full_path, nested_properties, visited_keys)?;
252			} else if let Some(default_value) = prop_details.get("default") {
253				// Handle regular property with default value
254				merged_defaults.insert(full_path.clone(), default_value.clone());
255			}
256		}
257
258		// Remove current key from visited keys
259		visited_keys.retain(|k| k != &full_path);
260	}
261
262	Ok(())
263}