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}