Skip to main content

Mountain/Environment/ConfigurationProvider/
UpdateValue.rs

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