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	IPC::SkyEvent::SkyEvent,
33};
34use serde_json::{Map, Value};
35use tauri::Manager;
36
37use crate::{Environment::Utility, IPC::SkyEmit::LogSkyEmit, RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
38
39/// Updates a configuration value in the appropriate `settings.json` file.
40pub(super) async fn update_configuration_value(
41	environment:&crate::Environment::MountainEnvironment::MountainEnvironment,
42
43	key:String,
44
45	value:Value,
46
47	target:ConfigurationTarget,
48
49	overrides:ConfigurationOverridesDTO,
50
51	_scope_to_language:Option<bool>,
52) -> Result<(), CommonError> {
53	dev_log!(
54		"config",
55		"[ConfigurationProvider] Updating key '{}' in target {:?}",
56		key,
57		target
58	);
59
60	let runtime = environment.ApplicationHandle.state::<Arc<ApplicationRunTime>>().inner().clone();
61
62	let config_path:PathBuf = match target {
63		// Land treats `UserLocal` and `User` as the same `settings.json`
64		// at the app-config dir. Stock VS Code differentiates them when
65		// settings sync is on (UserLocal stays per-machine, User syncs);
66		// Land has no sync backend, so the distinction is moot.
67		ConfigurationTarget::UserLocal | ConfigurationTarget::User => {
68			environment
69				.ApplicationHandle
70				.path()
71				.app_config_dir()
72				.map(|p| p.join("settings.json"))
73				.map_err(|error| {
74					CommonError::ConfigurationLoad {
75						Description:format!("Could not resolve user config path: {}", error),
76					}
77				})?
78		},
79
80		ConfigurationTarget::Workspace => {
81			environment
82				.ApplicationState
83				.Workspace
84				.WorkspaceConfigurationPath
85				.lock()
86				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
87				.clone()
88				.ok_or_else(|| {
89					CommonError::ConfigurationLoad { Description:"No workspace configuration path set".into() }
90				})?
91		},
92
93		// `WorkspaceFolder` (multi-root) - write to
94		// `<folder>/.vscode/settings.json` of the first workspace
95		// folder. Multi-root extensions should pass the folder URI
96		// in `_overrides.resource`; until that's plumbed through the
97		// trait the first folder is the closest stable approximation.
98		ConfigurationTarget::WorkspaceFolder => {
99			let FoldersGuard = environment
100				.ApplicationState
101				.Workspace
102				.WorkspaceFolders
103				.lock()
104				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
105
106			let First = FoldersGuard.first().ok_or_else(|| {
107				CommonError::ConfigurationLoad {
108					Description:"No workspace folders open for WorkspaceFolder target".into(),
109				}
110			})?;
111
112			let FolderPath = First.URI.to_file_path().map_err(|_| {
113				CommonError::ConfigurationLoad {
114					Description:format!("Workspace folder URI is not a local path: {}", First.URI),
115				}
116			})?;
117
118			FolderPath.join(".vscode").join("settings.json")
119		},
120
121		// `Memory` target only updates the in-memory configuration
122		// state for the lifetime of the session - no disk write.
123		// `SetGlobalValue` writes into the merged-config DTO; the
124		// DTO is the same map `GetValue` reads from, so subsequent
125		// `Inspect` / `Get` calls reflect the override immediately.
126		ConfigurationTarget::Memory => {
127			environment.ApplicationState.Configuration.SetGlobalValue(&key, value.clone());
128
129			dev_log!(
130				"config",
131				"[ConfigurationProvider] Memory target: stored in-memory value for '{}'",
132				key
133			);
134
135			return Ok(());
136		},
137
138		// `Default` and `Policy` are read-only by spec.
139		ConfigurationTarget::Default | ConfigurationTarget::Policy => {
140			return Err(CommonError::InvalidArgument {
141				ArgumentName:"target".into(),
142				Reason:format!("Configuration target {:?} is read-only", target),
143			});
144		},
145	};
146
147	// Read the file, modify it, and write it back.
148	let bytes = runtime.Run(ReadFile(config_path.clone())).await.unwrap_or_default();
149
150	let mut current_config:Value = serde_json::from_slice(&bytes).unwrap_or_else(|_| Value::Object(Map::new()));
151
152	if let Value::Object(ref mut RootMap) = current_config {
153		if let Some(LangId) = overrides.OverrideIdentifier.as_deref().filter(|S| !S.is_empty()) {
154			// Language-scoped override: write into `"[<langId>]": { key: value }`.
155			// This is how VS Code stores per-language defaults:
156			//   `prettier-vscode` sets `"[typescript]": { "editor.defaultFormatter": "..."
157			// }`   `vscode-eslint` sets `"[javascript]": { "editor.codeActionsOnSave":
158			// {...} }`
159			let ScopeKey = format!("[{}]", LangId);
160
161			let LangScope = RootMap.entry(ScopeKey.clone()).or_insert_with(|| Value::Object(Map::new()));
162
163			if let Value::Object(LangMap) = LangScope {
164				if value.is_null() {
165					LangMap.remove(&key);
166
167					if LangMap.is_empty() {
168						RootMap.remove(&ScopeKey);
169					}
170
171					dev_log!("config", "[ConfigurationProvider] Removed '[{}]' key '{}'", LangId, key);
172				} else {
173					LangMap.insert(key.clone(), value.clone());
174
175					dev_log!("config", "[ConfigurationProvider] Updated '[{}]' key '{}'", LangId, key);
176				}
177			}
178		} else {
179			// Top-level key - standard behaviour.
180			if value.is_null() {
181				RootMap.remove(&key);
182
183				dev_log!("config", "[ConfigurationProvider] Removed configuration key '{}'", key);
184			} else {
185				RootMap.insert(key.clone(), value.clone());
186
187				dev_log!("config", "[ConfigurationProvider] Updated configuration key '{}'", key);
188			}
189		}
190	}
191
192	let content_bytes = serde_json::to_vec_pretty(&current_config)?;
193
194	runtime
195		.Run(WriteFileBytes(config_path.clone(), content_bytes, true, true))
196		.await?;
197
198	// Invalidate the parsed-settings.json cache so the very next
199	// Inspect / merge re-reads from disk. Without this, the cached
200	// parse from before this update could stick around for up to
201	// 250 ms and feed stale values to the workbench until expiry.
202	crate::Environment::ConfigurationProvider::Loading::ClearSettingsFileCache();
203
204	// Re-merge all configurations to update the live state.
205	crate::Environment::ConfigurationProvider::Loading::Fn(environment).await?;
206
207	// Notify Sky (the VS Code workbench) so its ConfigurationService rebuilds
208	// its cached model. Without this the Settings UI and workspace inspectors
209	// don't reflect extension-triggered writes until a window reload.
210	let EmitPayload = serde_json::json!({ "keys": [key] });
211
212	if let Err(Error) = LogSkyEmit(
213		&environment.ApplicationHandle,
214		SkyEvent::ConfigurationChanged.AsStr(),
215		EmitPayload,
216	) {
217		dev_log!(
218			"config",
219			"warn: [ConfigurationProvider] sky://configuration/changed emit failed: {}",
220			Error
221		);
222	}
223
224	// Also notify Cocoon's extension host so its ConfigCache invalidates the
225	// affected key and re-primes. Without this, extensions calling
226	// `workspace.getConfiguration().get(key)` after an update continue reading
227	// the stale cached value until the next full re-merge notification.
228	// Using fire-and-forget: the write is already durable; a Vine failure here
229	// must not roll back the successful disk write.
230	let NotifyKey = key.clone();
231
232	tokio::spawn(async move {
233		if let Err(Error) = crate::Vine::Client::SendNotification::Fn(
234			"cocoon-main".to_string(),
235			"configuration.change".to_string(),
236			serde_json::json!({ "keys": [NotifyKey] }),
237		)
238		.await
239		{
240			crate::dev_log!(
241				"config",
242				"warn: [ConfigurationProvider] configuration.change Cocoon notify failed: {:?}",
243				Error
244			);
245		}
246	});
247
248	Ok(())
249}