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}