Skip to main content

Mountain/Vine/Server/Notification/
RegisterScmProvider.rs

1#![allow(non_snake_case)]
2//! Cocoon → Mountain `register_scm_provider` notification.
3//!
4//! Replaces the previous behaviour where this wire-method fell through
5//! the language-providers OR-block in `MountainVinegRPCService.rs` and
6//! got registered as a `ProviderType::SourceControl` *language* provider
7//! (wrong - the SCM viewlet binds to `ApplicationState::SourceControl`,
8//! not the language-feature provider registry, so the panel stayed
9//! empty even though `vscode.scm.createSourceControl(...)` succeeded
10//! inside Cocoon).
11//!
12//! Cocoon emits this from `ScmNamespace.ts:14` with payload shape:
13//!
14//! ```ignore
15//! { handle: u32, id, label, root_uri, extension_id }
16//! ```
17//!
18//! Three side effects happen here:
19//!   1. `ProviderRegistration::RegisterProvider` records the handle so future
20//!      language-feature dispatches that look up by SCM handle (rare but
21//!      possible) resolve.
22//!   2. `SourceControlManagementProvider::CreateSourceControl` mutates
23//!      `ApplicationState::Feature::Markers::SourceControlManagementProviders`
24//!      and emits `SkyEvent::SCMProviderAdded` - this is the canonical
25//!      state-tracking path the SCM view uses.
26//!   3. A direct `sky://scm/register` Tauri emit covers any renderer path that
27//!      listens for the simpler legacy event shape (gitlens, future custom SCM
28//!      views).
29//!
30//! All three are best-effort and independent: the trait call may fail
31//! when `root_uri` is missing (extensions occasionally register an SCM
32//! before opening a folder); the registry write is infallible; the
33//! Sky emit is fire-and-forget.
34
35use serde_json::{Value, json};
36// `tauri::Emitter` previously imported for direct `.emit()` calls;
37// emits now route through `LogSkyEmit` which carries the trait. No
38// remaining `.emit()` callsites in this file.
39use CommonLibrary::SourceControlManagement::SourceControlManagementProvider::SourceControlManagementProvider;
40
41use crate::{
42	ApplicationState::DTO::ProviderRegistrationDTO::ProviderRegistrationDTO,
43	Vine::Server::MountainVinegRPCService::MountainVinegRPCService,
44	dev_log,
45};
46
47pub async fn RegisterScmProvider(Service:&MountainVinegRPCService, Parameter:&Value) {
48	// Wire-shape contract: producer (`Cocoon/.../ScmNamespace.ts`) emits
49	// camelCase keys (`rootUri`, `extensionId`) post 2026-04-27 wire audit.
50	// Probe camelCase first; keep snake_case as a transitional fallback so
51	// a partial rebuild (Mountain ahead of Cocoon) doesn't silently drop.
52	let ScmId = Parameter
53		.get("id")
54		.or_else(|| Parameter.get("scmId"))
55		.or_else(|| Parameter.get("scm_id"))
56		.and_then(Value::as_str)
57		.unwrap_or("")
58		.to_string();
59
60	let Label = Parameter.get("label").and_then(Value::as_str).unwrap_or(&ScmId).to_string();
61
62	let ExtensionId = Parameter
63		.get("extensionId")
64		.or_else(|| Parameter.get("extension_id"))
65		.and_then(Value::as_str)
66		.unwrap_or("")
67		.to_string();
68
69	let RootUri = Parameter
70		.get("rootUri")
71		.or_else(|| Parameter.get("root_uri"))
72		.cloned()
73		.unwrap_or(Value::Null);
74
75	if ScmId.is_empty() {
76		dev_log!("provider-register", "[ProviderRegister] scm skip: missing scm_id");
77
78		return;
79	}
80
81	// Cocoon's `ScmNamespace.ts` uses a process-local sequential
82	// `NextProviderHandle()` and includes that handle on the wire
83	// payload. Subsequent `register_scm_resource_group`,
84	// `update_scm_group`, and `unregister_scm_provider` notifications
85	// reference the SAME sequential handle as `scm_handle`, so we must
86	// preserve it here verbatim - otherwise the registry write below
87	// keys under DJB-hash-of-id and the resource-group/update path
88	// keys under Cocoon's sequential, and the SCM viewlet sees a
89	// provider with no groups regardless of how many resources arrive.
90	//
91	// Fall back to the DJB hash only when Cocoon (or a third-party
92	// caller) omits the field, so this keeps working with the legacy
93	// shape without forcing a Cocoon upgrade.
94	let Handle = Parameter
95		.get("handle")
96		.or_else(|| Parameter.get("scmHandle"))
97		.or_else(|| Parameter.get("scm_handle"))
98		.and_then(Value::as_u64)
99		.map(|H| H as u32)
100		.unwrap_or_else(|| {
101			ScmId
102				.as_bytes()
103				.iter()
104				.fold(0u32, |Acc, B| Acc.wrapping_mul(31).wrapping_add(*B as u32))
105		});
106
107	use CommonLibrary::LanguageFeature::DTO::ProviderType::ProviderType;
108
109	let RegistrationDto = ProviderRegistrationDTO {
110		Handle,
111
112		ProviderType:ProviderType::SourceControl,
113
114		Selector:json!([{ "scmId": &ScmId }]),
115
116		SideCarIdentifier:"cocoon-main".to_string(),
117
118		ExtensionIdentifier:json!(&ExtensionId),
119
120		Options:Some(json!({ "scmId": &ScmId, "label": &Label })),
121	};
122
123	Service
124		.RunTime()
125		.Environment
126		.ApplicationState
127		.Extension
128		.ProviderRegistration
129		.RegisterProvider(Handle, RegistrationDto);
130
131	// Trait wiring populates `ApplicationState::Feature::Markers`
132	// + emits the typed `SkyEvent::SCMProviderAdded`. RootUri is
133	// expected to be a parseable URL string; when extensions pass null
134	// (rare - usually a workspace folder URI) we substitute the empty
135	// `file:///` so the trait still records the provider.
136	//
137	// vscode.git's `repository.ts:983` calls `Uri.file(repository.root)`
138	// which serialises to a UriComponents object: `{scheme:"file",
139	// authority:"", path:"/Volumes/...", query:"", fragment:""}`. The
140	// previous extractor read `O.get("path")` which is the **path
141	// component only** (no scheme prefix) and passed it through to
142	// `URLSerializationHelper`'s `Url::parse(...)`, which fails with
143	// "relative URL without a base" because `/Volumes/...` has no
144	// scheme. Rebuild a proper `<scheme>://<authority><path>` triple
145	// from the components first; only fall back to `external` (already
146	// a string URL) or `path` if the triple can't be assembled.
147	let BuildUrlFromComponents = |O:&serde_json::Map<String, Value>| -> Option<String> {
148		let Scheme = O.get("scheme").and_then(Value::as_str)?;
149
150		if Scheme.is_empty() {
151			return None;
152		}
153
154		let Authority = O.get("authority").and_then(Value::as_str).unwrap_or("");
155
156		let Path = O.get("path").and_then(Value::as_str).unwrap_or("");
157
158		let Query = O.get("query").and_then(Value::as_str).unwrap_or("");
159
160		let Fragment = O.get("fragment").and_then(Value::as_str).unwrap_or("");
161
162		let mut Url = format!("{}://{}{}", Scheme, Authority, Path);
163
164		if !Query.is_empty() {
165			Url.push('?');
166
167			Url.push_str(Query);
168		}
169
170		if !Fragment.is_empty() {
171			Url.push('#');
172
173			Url.push_str(Fragment);
174		}
175
176		Some(Url)
177	};
178
179	let RootUriString = match &RootUri {
180		Value::String(S) => S.clone(),
181
182		Value::Object(O) => {
183			BuildUrlFromComponents(O)
184				.or_else(|| O.get("external").and_then(Value::as_str).map(str::to_string))
185				.or_else(|| {
186					// Last-resort: prepend file:// to a bare path so
187					// URLSerializationHelper at least gets a parseable
188					// scheme. Never silently emit a relative URL.
189					O.get("path")
190						.and_then(Value::as_str)
191						.filter(|P| P.starts_with('/'))
192						.map(|P| format!("file://{}", P))
193				})
194				.unwrap_or_else(|| "file:///".to_string())
195		},
196
197		_ => "file:///".to_string(),
198	};
199
200	// Field names must match `SourceControlCreateDTO`'s camelCase wire
201	// shape (post-DTO-audit): `id`, `label`, `rootUri`. Earlier revisions
202	// passed PascalCase keys here and the trait silently failed with
203	// `missing field "id"` because the DTO's serde rename uses camelCase.
204	//
205	// `handle` is the Cocoon-allocated sequential provider handle (read
206	// above from the Parameter). Including it on the wire makes
207	// `MountainEnvironment::CreateSourceControl` key its marker maps
208	// under the SAME handle that subsequent `register_scm_resource_group`
209	// and `update_scm_group` notifications reference - without this,
210	// every group update warns "Received group update for unknown
211	// provider handle: <H>" because the marker map was keyed by a
212	// fresh Mountain-allocated handle Cocoon never sees.
213	let CreateData = json!({
214		"handle": Handle,
215		"id": &ScmId,
216		"label": &Label,
217		"rootUri": RootUriString,
218	});
219
220	if let Err(Error) = Service.RunTime().Environment.CreateSourceControl(CreateData).await {
221		dev_log!("grpc", "warn: [Scm] CreateSourceControl trait failed for {}: {}", ScmId, Error);
222	}
223
224	// Legacy listener channel kept active alongside the typed event so
225	// renderer code that hasn't migrated to the markers-backed view
226	// (gitlens-side custom panels, hand-rolled tests) still sees the
227	// register signal. Routed through `LogSkyEmit` so `sky-emit` /
228	// `grpc` dev-log tags surface delivery success/failure - the
229	// fire-and-forget path was previously invisible, making it
230	// impossible to tell whether Sky's `Register("sky://scm/register")`
231	// listener was hit when the SCM panel stayed empty.
232	if let Err(Error) = crate::IPC::SkyEmit::LogSkyEmit(
233		Service.ApplicationHandle(),
234		"sky://scm/register",
235		json!({
236			"scmId": &ScmId,
237			"label": &Label,
238			"rootUri": &RootUriString,
239			"extensionId": &ExtensionId,
240			"handle": Handle,
241		}),
242	) {
243		dev_log!("grpc", "warn: [Scm] sky://scm/register emit failed for {}: {}", ScmId, Error);
244	}
245
246	dev_log!(
247		"grpc",
248		"[Scm] register provider scmId={} label={} ext={} handle={}",
249		ScmId,
250		Label,
251		ExtensionId,
252		Handle
253	);
254}