Skip to main content

Vine/Server/Notification/
RegisterScmProvider.rs

1//! Cocoon → `register_scm_provider` notification.
2//!
3//! Three side effects, all best-effort and independent:
4//! 1. `VineHost::RegisterScmInRegistry` records the handle in the embedder's
5//!    `ProviderRegistration` table so future handle-keyed dispatches resolve.
6//! 2. `VineHost::CreateSourceControl` mutates the embedder's SCM marker state
7//!    and emits `SkyEvent::SCMProviderAdded` - the canonical path the SCM view
8//!    uses.
9//! 3. `Host.EmitToRenderer("sky://scm/register", ...)` covers renderer code
10//!    that listens for the legacy simpler event shape.
11//!
12//! Handle disambiguation: Cocoon's `ScmNamespace.ts` allocates a
13//! process-local sequential handle and includes it on the wire. Subsequent
14//! `register_scm_resource_group`, `update_scm_group`, and
15//! `unregister_scm_provider` notifications reference the SAME sequential
16//! handle. Falling back to a DJB hash of `ScmId` is only allowed when
17//! Cocoon omits the field (legacy callers).
18
19use serde_json::{Value, json};
20
21use crate::{Host::VineHost, dev_log};
22
23/// Rebuilds a full URL string from a VS Code `UriComponents` JSON object
24/// (`{ scheme, authority, path, query, fragment }`). Returns `None` when
25/// the scheme field is absent or empty.
26fn BuildUrlFromComponents(O:&serde_json::Map<String, Value>) -> Option<String> {
27	let Scheme = O.get("scheme").and_then(Value::as_str)?;
28
29	if Scheme.is_empty() {
30		return None;
31	}
32
33	let Authority = O.get("authority").and_then(Value::as_str).unwrap_or("");
34
35	let Path = O.get("path").and_then(Value::as_str).unwrap_or("");
36
37	let Query = O.get("query").and_then(Value::as_str).unwrap_or("");
38
39	let Fragment = O.get("fragment").and_then(Value::as_str).unwrap_or("");
40
41	let mut Url = format!("{}://{}{}", Scheme, Authority, Path);
42
43	if !Query.is_empty() {
44		Url.push('?');
45
46		Url.push_str(Query);
47	}
48
49	if !Fragment.is_empty() {
50		Url.push('#');
51
52		Url.push_str(Fragment);
53	}
54
55	Some(Url)
56}
57
58pub async fn RegisterScmProvider(Host:&dyn VineHost, Parameter:&Value) {
59	// Wire-shape: camelCase first (current Cocoon), snake_case fallback for
60	// transitional compatibility when Mountain is ahead of Cocoon.
61	let ScmId = Parameter
62		.get("id")
63		.or_else(|| Parameter.get("scmId"))
64		.or_else(|| Parameter.get("scm_id"))
65		.and_then(Value::as_str)
66		.unwrap_or("")
67		.to_string();
68
69	let Label = Parameter.get("label").and_then(Value::as_str).unwrap_or(&ScmId).to_string();
70
71	let ExtensionId = Parameter
72		.get("extensionId")
73		.or_else(|| Parameter.get("extension_id"))
74		.and_then(Value::as_str)
75		.unwrap_or("")
76		.to_string();
77
78	let RootUri = Parameter
79		.get("rootUri")
80		.or_else(|| Parameter.get("root_uri"))
81		.cloned()
82		.unwrap_or(Value::Null);
83
84	if ScmId.is_empty() {
85		dev_log!("provider-register", "[ProviderRegister] scm skip: missing scm_id");
86
87		return;
88	}
89
90	// Preserve Cocoon's sequential handle verbatim so all subsequent
91	// resource-group / update notifications key under the same value.
92	let Handle = Parameter
93		.get("handle")
94		.or_else(|| Parameter.get("scmHandle"))
95		.or_else(|| Parameter.get("scm_handle"))
96		.and_then(Value::as_u64)
97		.map(|H| H as u32)
98		.unwrap_or_else(|| {
99			ScmId
100				.as_bytes()
101				.iter()
102				.fold(0u32, |Acc, B| Acc.wrapping_mul(31).wrapping_add(*B as u32))
103		});
104
105	// Side effect 1 - register in provider table.
106	Host.RegisterScmInRegistry(Handle, &ScmId, &Label, &ExtensionId);
107
108	// Reconstruct a full URL from a VS Code UriComponents object if needed.
109	let RootUriString = match &RootUri {
110		Value::String(S) => S.clone(),
111
112		Value::Object(O) => {
113			BuildUrlFromComponents(O)
114				.or_else(|| O.get("external").and_then(Value::as_str).map(str::to_string))
115				.or_else(|| {
116					O.get("path")
117						.and_then(Value::as_str)
118						.filter(|P| P.starts_with('/'))
119						.map(|P| format!("file://{}", P))
120				})
121				.unwrap_or_else(|| "file:///".to_string())
122		},
123
124		_ => "file:///".to_string(),
125	};
126
127	// Field names must match `SourceControlCreateDTO`'s camelCase wire shape.
128	// Including `handle` here makes `MountainEnvironment::CreateSourceControl`
129	// key its marker maps under the SAME handle that
130	// `register_scm_resource_group` and `update_scm_group` reference.
131	let CreateData = json!({
132		"handle": Handle,
133		"id": &ScmId,
134		"label": &Label,
135		"rootUri": RootUriString,
136	});
137
138	// Side effect 2 - SCM provider state + SkyEvent::SCMProviderAdded.
139	Host.CreateSourceControl(CreateData).await;
140
141	// Side effect 3 - legacy renderer channel.
142	Host.EmitToRenderer(
143		"sky://scm/register",
144		json!({
145			"scmId": &ScmId,
146			"label": &Label,
147			"rootUri": &RootUriString,
148			"extensionId": &ExtensionId,
149			"handle": Handle,
150		}),
151	);
152
153	dev_log!(
154		"grpc",
155		"[Scm] register provider scmId={} label={} ext={} handle={}",
156		ScmId,
157		Label,
158		ExtensionId,
159		Handle
160	);
161}