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	let Label = Parameter.get("label").and_then(Value::as_str).unwrap_or(&ScmId).to_string();
60	let ExtensionId = Parameter
61		.get("extensionId")
62		.or_else(|| Parameter.get("extension_id"))
63		.and_then(Value::as_str)
64		.unwrap_or("")
65		.to_string();
66	let RootUri = Parameter
67		.get("rootUri")
68		.or_else(|| Parameter.get("root_uri"))
69		.cloned()
70		.unwrap_or(Value::Null);
71
72	if ScmId.is_empty() {
73		dev_log!("provider-register", "[ProviderRegister] scm skip: missing scm_id");
74		return;
75	}
76
77	// Cocoon's `ScmNamespace.ts` uses a process-local sequential
78	// `NextProviderHandle()` and includes that handle on the wire
79	// payload. Subsequent `register_scm_resource_group`,
80	// `update_scm_group`, and `unregister_scm_provider` notifications
81	// reference the SAME sequential handle as `scm_handle`, so we must
82	// preserve it here verbatim - otherwise the registry write below
83	// keys under DJB-hash-of-id and the resource-group/update path
84	// keys under Cocoon's sequential, and the SCM viewlet sees a
85	// provider with no groups regardless of how many resources arrive.
86	//
87	// Fall back to the DJB hash only when Cocoon (or a third-party
88	// caller) omits the field, so this keeps working with the legacy
89	// shape without forcing a Cocoon upgrade.
90	let Handle = Parameter
91		.get("handle")
92		.or_else(|| Parameter.get("scmHandle"))
93		.or_else(|| Parameter.get("scm_handle"))
94		.and_then(Value::as_u64)
95		.map(|H| H as u32)
96		.unwrap_or_else(|| {
97			ScmId
98				.as_bytes()
99				.iter()
100				.fold(0u32, |Acc, B| Acc.wrapping_mul(31).wrapping_add(*B as u32))
101		});
102
103	use CommonLibrary::LanguageFeature::DTO::ProviderType::ProviderType;
104	let RegistrationDto = ProviderRegistrationDTO {
105		Handle,
106		ProviderType:ProviderType::SourceControl,
107		Selector:json!([{ "scmId": &ScmId }]),
108		SideCarIdentifier:"cocoon-main".to_string(),
109		ExtensionIdentifier:json!(&ExtensionId),
110		Options:Some(json!({ "scmId": &ScmId, "label": &Label })),
111	};
112	Service
113		.RunTime()
114		.Environment
115		.ApplicationState
116		.Extension
117		.ProviderRegistration
118		.RegisterProvider(Handle, RegistrationDto);
119
120	// Trait wiring populates `ApplicationState::Feature::Markers`
121	// + emits the typed `SkyEvent::SCMProviderAdded`. RootUri is
122	// expected to be a parseable URL string; when extensions pass null
123	// (rare - usually a workspace folder URI) we substitute the empty
124	// `file:///` so the trait still records the provider.
125	//
126	// vscode.git's `repository.ts:983` calls `Uri.file(repository.root)`
127	// which serialises to a UriComponents object: `{scheme:"file",
128	// authority:"", path:"/Volumes/...", query:"", fragment:""}`. The
129	// previous extractor read `O.get("path")` which is the **path
130	// component only** (no scheme prefix) and passed it through to
131	// `URLSerializationHelper`'s `Url::parse(...)`, which fails with
132	// "relative URL without a base" because `/Volumes/...` has no
133	// scheme. Rebuild a proper `<scheme>://<authority><path>` triple
134	// from the components first; only fall back to `external` (already
135	// a string URL) or `path` if the triple can't be assembled.
136	let BuildUrlFromComponents = |O:&serde_json::Map<String, Value>| -> Option<String> {
137		let Scheme = O.get("scheme").and_then(Value::as_str)?;
138		if Scheme.is_empty() {
139			return None;
140		}
141		let Authority = O.get("authority").and_then(Value::as_str).unwrap_or("");
142		let Path = O.get("path").and_then(Value::as_str).unwrap_or("");
143		let Query = O.get("query").and_then(Value::as_str).unwrap_or("");
144		let Fragment = O.get("fragment").and_then(Value::as_str).unwrap_or("");
145		let mut Url = format!("{}://{}{}", Scheme, Authority, Path);
146		if !Query.is_empty() {
147			Url.push('?');
148			Url.push_str(Query);
149		}
150		if !Fragment.is_empty() {
151			Url.push('#');
152			Url.push_str(Fragment);
153		}
154		Some(Url)
155	};
156	let RootUriString = match &RootUri {
157		Value::String(S) => S.clone(),
158		Value::Object(O) => {
159			BuildUrlFromComponents(O)
160				.or_else(|| O.get("external").and_then(Value::as_str).map(str::to_string))
161				.or_else(|| {
162					// Last-resort: prepend file:// to a bare path so
163					// URLSerializationHelper at least gets a parseable
164					// scheme. Never silently emit a relative URL.
165					O.get("path")
166						.and_then(Value::as_str)
167						.filter(|P| P.starts_with('/'))
168						.map(|P| format!("file://{}", P))
169				})
170				.unwrap_or_else(|| "file:///".to_string())
171		},
172		_ => "file:///".to_string(),
173	};
174	// Field names must match `SourceControlCreateDTO`'s camelCase wire
175	// shape (post-DTO-audit): `id`, `label`, `rootUri`. Earlier revisions
176	// passed PascalCase keys here and the trait silently failed with
177	// `missing field "id"` because the DTO's serde rename uses camelCase.
178	//
179	// `handle` is the Cocoon-allocated sequential provider handle (read
180	// above from the Parameter). Including it on the wire makes
181	// `MountainEnvironment::CreateSourceControl` key its marker maps
182	// under the SAME handle that subsequent `register_scm_resource_group`
183	// and `update_scm_group` notifications reference - without this,
184	// every group update warns "Received group update for unknown
185	// provider handle: <H>" because the marker map was keyed by a
186	// fresh Mountain-allocated handle Cocoon never sees.
187	let CreateData = json!({
188		"handle": Handle,
189		"id": &ScmId,
190		"label": &Label,
191		"rootUri": RootUriString,
192	});
193	if let Err(Error) = Service.RunTime().Environment.CreateSourceControl(CreateData).await {
194		dev_log!("grpc", "warn: [Scm] CreateSourceControl trait failed for {}: {}", ScmId, Error);
195	}
196
197	// Legacy listener channel kept active alongside the typed event so
198	// renderer code that hasn't migrated to the markers-backed view
199	// (gitlens-side custom panels, hand-rolled tests) still sees the
200	// register signal. Routed through `LogSkyEmit` so `sky-emit` /
201	// `grpc` dev-log tags surface delivery success/failure - the
202	// fire-and-forget path was previously invisible, making it
203	// impossible to tell whether Sky's `Register("sky://scm/register")`
204	// listener was hit when the SCM panel stayed empty.
205	if let Err(Error) = crate::IPC::SkyEmit::LogSkyEmit(
206		Service.ApplicationHandle(),
207		"sky://scm/register",
208		json!({
209			"scmId": &ScmId,
210			"label": &Label,
211			"rootUri": &RootUriString,
212			"extensionId": &ExtensionId,
213			"handle": Handle,
214		}),
215	) {
216		dev_log!("grpc", "warn: [Scm] sky://scm/register emit failed for {}: {}", ScmId, Error);
217	}
218
219	dev_log!(
220		"grpc",
221		"[Scm] register provider scmId={} label={} ext={} handle={}",
222		ScmId,
223		Label,
224		ExtensionId,
225		Handle
226	);
227}