Skip to main content

Mountain/Vine/Server/Notification/
RegisterScmProvider.rs

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