Skip to main content

Mountain/Environment/ConfigurationProvider/
UpdateValue.rs

1//! Configuration value update and persistence.
2
3use std::{path::PathBuf, sync::Arc};
4
5use CommonLibrary::{
6	Configuration::DTO::{
7		ConfigurationOverridesDTO::ConfigurationOverridesDTO,
8		ConfigurationTarget::ConfigurationTarget,
9	},
10	Effect::ApplicationRunTime::ApplicationRunTime as _,
11	Error::CommonError::CommonError,
12	FileSystem::{ReadFile::ReadFile, WriteFileBytes::WriteFileBytes},
13};
14use serde_json::{Map, Value};
15use tauri::Manager;
16
17use crate::{Environment::Utility, RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
18
19/// Updates a configuration value in the appropriate `settings.json` file.
20pub(super) async fn update_configuration_value(
21	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
22	key:String,
23	value:Value,
24	target:ConfigurationTarget,
25	_overrides:ConfigurationOverridesDTO,
26	_scope_to_language:Option<bool>,
27) -> Result<(), CommonError> {
28	dev_log!(
29		"config",
30		"[ConfigurationProvider] Updating key '{}' in target {:?}",
31		key,
32		target
33	);
34
35	let runtime = environment.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
36
37	let config_path:PathBuf = match target {
38		// Land treats `UserLocal` and `User` as the same `settings.json`
39		// at the app-config dir. Stock VS Code differentiates them when
40		// settings sync is on (UserLocal stays per-machine, User syncs);
41		// Land has no sync backend, so the distinction is moot.
42		ConfigurationTarget::UserLocal | ConfigurationTarget::User => {
43			environment
44				.ApplicationHandle
45				.path()
46				.app_config_dir()
47				.map(|p| p.join("settings.json"))
48				.map_err(|error| {
49					CommonError::ConfigurationLoad {
50						Description:format!("Could not resolve user config path: {}", error),
51					}
52				})?
53		},
54
55		ConfigurationTarget::Workspace => {
56			environment
57				.ApplicationState
58				.Workspace
59				.WorkspaceConfigurationPath
60				.lock()
61				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
62				.clone()
63				.ok_or_else(|| {
64					CommonError::ConfigurationLoad { Description:"No workspace configuration path set".into() }
65				})?
66		},
67
68		// `WorkspaceFolder` (multi-root) - write to
69		// `<folder>/.vscode/settings.json` of the first workspace
70		// folder. Multi-root extensions should pass the folder URI
71		// in `_overrides.resource`; until that's plumbed through the
72		// trait the first folder is the closest stable approximation.
73		ConfigurationTarget::WorkspaceFolder => {
74			let FoldersGuard = environment
75				.ApplicationState
76				.Workspace
77				.WorkspaceFolders
78				.lock()
79				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
80			let First = FoldersGuard.first().ok_or_else(|| {
81				CommonError::ConfigurationLoad {
82					Description:"No workspace folders open for WorkspaceFolder target".into(),
83				}
84			})?;
85			let FolderPath = First.URI.to_file_path().map_err(|_| {
86				CommonError::ConfigurationLoad {
87					Description:format!("Workspace folder URI is not a local path: {}", First.URI),
88				}
89			})?;
90			FolderPath.join(".vscode").join("settings.json")
91		},
92
93		// `Memory` target only updates the in-memory configuration
94		// state for the lifetime of the session - no disk write.
95		// `SetGlobalValue` writes into the merged-config DTO; the
96		// DTO is the same map `GetValue` reads from, so subsequent
97		// `Inspect` / `Get` calls reflect the override immediately.
98		ConfigurationTarget::Memory => {
99			environment.ApplicationState.Configuration.SetGlobalValue(&key, value.clone());
100			dev_log!(
101				"config",
102				"[ConfigurationProvider] Memory target: stored in-memory value for '{}'",
103				key
104			);
105			return Ok(());
106		},
107
108		// `Default` and `Policy` are read-only by spec.
109		ConfigurationTarget::Default | ConfigurationTarget::Policy => {
110			return Err(CommonError::InvalidArgument {
111				ArgumentName:"target".into(),
112				Reason:format!("Configuration target {:?} is read-only", target),
113			});
114		},
115	};
116
117	// Read the file, modify it, and write it back.
118	let bytes = runtime.Run(ReadFile(config_path.clone())).await.unwrap_or_default();
119
120	let mut current_config:Value = serde_json::from_slice(&bytes).unwrap_or_else(|_| Value::Object(Map::new()));
121
122	if let Value::Object(map) = &mut current_config {
123		if value.is_null() {
124			map.remove(&key);
125			dev_log!("config", "[ConfigurationProvider] Removed configuration key '{}'", key);
126		} else {
127			map.insert(key.clone(), value.clone());
128			dev_log!("config", "[ConfigurationProvider] Updated configuration key '{}'", key);
129		}
130	}
131
132	let content_bytes = serde_json::to_vec_pretty(&current_config)?;
133
134	runtime
135		.Run(WriteFileBytes(config_path.clone(), content_bytes, true, true))
136		.await?;
137
138	// Invalidate the parsed-settings.json cache so the very next
139	// Inspect / merge re-reads from disk. Without this, the cached
140	// parse from before this update could stick around for up to
141	// 250 ms and feed stale values to the workbench until expiry.
142	crate::Environment::ConfigurationProvider::Loading::ClearSettingsFileCache();
143
144	// Re-merge all configurations to update the live state.
145	crate::Environment::ConfigurationProvider::Loading::initialize_and_merge_configurations(environment).await?;
146
147	Ok(())
148}