Skip to main content

CommonLibrary/IPC/
SkyEvent.rs

1//! # Sky Event Registry - single source of truth for Mountain → Sky/Wind events
2//!
3//! Mountain emits Tauri events on `sky://…` URIs to notify the webview of
4//! state changes that don't originate from a Wind-initiated `invoke` call.
5//! Historically each emit site used a free-text string literal and each Wind
6//! listener matched against its own free-text string - drift was invisible
7//! until runtime (the listener simply never fired).
8//!
9//! `SkyEvent` is the enumerated registry. Mountain callers dispatch on the
10//! variant; the wire string is produced by `AsStr()` and parsed by `FromStr`.
11//! The matching TypeScript const object lives at
12//! `Element/Wind/Source/IPC/SkyEvent.ts` - kept in sync by convention, same
13//! protocol as the `Channel` registry.
14//!
15//! ## Adding a new event
16//!
17//! 1. Add the variant here AND in `Element/Wind/Source/IPC/SkyEvent.ts`.
18//! 2. Emit from Mountain:
19//!    `ApplicationHandle.emit(SkyEvent::TerminalData.AsStr(), Payload)`.
20//! 3. Subscribe from Wind: `IPCService.events(SkyEvent.TerminalData)`.
21//!
22//! ## Why a declarative macro?
23//!
24//! Same rationale as `Channel`: the variant → wire-string mapping is pure
25//! data. `DefineSkyEvents!` expands it into enum body + `AsStr` + `All` +
26//! `FromStr` in one pass so adding an event is a single-line change that
27//! compilers can't forget.
28
29macro_rules! DefineSkyEvents {
30
31	($($Variant:ident => $Wire:literal,)* $(,)?) => {
32
33		/// Enumerated Mountain → Sky/Wind event identifiers.
34		#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35		pub enum SkyEvent {
36
37			$($Variant,)*
38		}
39
40		impl SkyEvent {
41
42			/// Wire string produced on the Tauri event transport.
43			pub fn AsStr(&self) -> &'static str {
44
45				match self {
46
47					$(Self::$Variant => $Wire,)*
48				}
49			}
50
51			/// Full set of events, in declaration order.
52			pub fn All() -> &'static [Self] {
53
54				&[$(Self::$Variant,)*]
55			}
56		}
57
58		impl ::std::fmt::Display for SkyEvent {
59
60			fn fmt(&self, Formatter:&mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
61
62				Formatter.write_str(self.AsStr())
63			}
64		}
65
66		impl ::std::str::FromStr for SkyEvent {
67
68			type Err = ::std::string::String;
69
70			fn from_str(Wire:&str) -> ::std::result::Result<Self, Self::Err> {
71
72				match Wire {
73
74					$($Wire => Ok(Self::$Variant),)*
75					_ => Err(format!("unknown Sky event: {}", Wire)),
76				}
77			}
78		}
79	};
80}
81
82DefineSkyEvents! {
83
84	// --- Configuration ---
85	ConfigurationChanged                          => "sky://configuration/changed",
86
87	// --- Debug ---
88	DebugDapMessage                               => "sky://debug/dap-message",
89
90	DebugRegister                                 => "sky://debug/register",
91
92	DebugStart                                    => "sky://debug/start",
93
94	DebugStop                                     => "sky://debug/stop",
95
96	// --- Decorations (set-ranges) ---
97	// Fired when an extension calls `editor.setDecorations(type, ranges)`.
98	// Payload: `{ decorationTypeKey, uri, rangesOrOptions }`.
99	// Sky listener applies to Monaco via ICodeEditorService.
100	DecorationSetRanges                           => "sky://decoration/set-ranges",
101
102	// --- Diagnostics ---
103	DiagnosticsChanged                            => "sky://diagnostics/changed",
104
105	// --- CustomEditor ---
106	CustomEditorSaved                             => "sky://customEditor/saved",
107
108	// --- Dialog ---
109	DialogOpen                                    => "sky://dialog/open",
110
111	DialogSave                                    => "sky://dialog/save",
112
113	// --- Documents ---
114	DocumentsOpen                                 => "sky://documents/open",
115
116	DocumentsRenamed                              => "sky://documents/renamed",
117
118	DocumentsSaved                                => "sky://documents/saved",
119
120	// --- Editor ---
121	EditorApplyEdits                              => "sky://editor/applyEdits",
122
123	// Fired when an extension calls `editor.edit(editBuilder => { ... })`.
124	// Payload: `{ uri, edits: [{ range, text }] }`.
125	EditorApplyTextEdits                          => "sky://editor/apply-text-edits",
126
127	// Fired by Sky when the active text editor's selection changes.
128	// Payload: `{ uri, selections: [{ start, end, active, anchor }] }`.
129	// Mountain stores it and forwards to Cocoon via `window.didChangeTextEditorSelection`.
130	EditorSelectionChanged                        => "sky://editor/selection-changed",
131
132	EditorOpenDocument                            => "sky://editor/openDocument",
133
134	EditorSaveAll                                 => "sky://editor/saveAll",
135
136	// Fired by Sky when a text editor becomes visible (tab activated).
137	// Payload: `{ uri, viewColumn, selections }`.
138	EditorActiveChanged                           => "sky://editor/active-changed",
139
140	// `editor.revealRange(range, revealType)` - scroll Monaco to the range.
141	// Payload: `{ uri, range, revealType }`.
142	EditorRevealRange                             => "sky://editor/revealRange",
143
144	// --- Extensions ---
145	ExtensionsInstalled                           => "sky://extensions/installed",
146
147	ExtensionsUninstalled                         => "sky://extensions/uninstalled",
148
149	// --- ExtHost ---
150	ExtHostDebugClose                             => "sky://exthost/debug-close",
151
152	ExtHostDebugReload                            => "sky://exthost/debug-reload",
153
154	// --- Input ---
155	InputBoxShow                                  => "sky://input-box/show",
156
157	// --- Language ---
158	LanguageConfigure                             => "sky://language/configure",
159
160	LanguagesSetDocumentLanguage                  => "sky://languages/setDocumentLanguage",
161
162	// --- Lifecycle ---
163	LifecyclePhaseChanged                         => "sky://lifecycle/phaseChanged",
164
165	LifecycleWillShutdown                         => "sky://lifecycle/willShutdown",
166
167	// --- Native ---
168	NativeOpenExternal                            => "sky://native/openExternal",
169
170	// --- Notifications ---
171	NotificationProgressBegin                     => "sky://notification/progress-begin",
172
173	NotificationProgressEnd                       => "sky://notification/progress-end",
174
175	NotificationProgressUpdate                    => "sky://notification/progress-update",
176
177	NotificationShow                              => "sky://notification/show",
178
179	// --- Output ---
180	OutputAppend                                  => "sky://output/append",
181
182	OutputClear                                   => "sky://output/clear",
183
184	OutputCreate                                  => "sky://output/create",
185
186	OutputDispose                                 => "sky://output/dispose",
187
188	OutputReplace                                 => "sky://output/replace",
189
190	OutputReveal                                  => "sky://output/reveal",
191
192	OutputShow                                    => "sky://output/show",
193
194	// --- Progress ---
195	ProgressBegin                                 => "sky://progress/begin",
196
197	ProgressComplete                              => "sky://progress/complete",
198
199	ProgressEnd                                   => "sky://progress/end",
200
201	ProgressReport                                => "sky://progress/report",
202
203	ProgressStart                                 => "sky://progress/start",
204
205	ProgressUpdate                                => "sky://progress/update",
206
207	// --- QuickPick ---
208	QuickPickShow                                 => "sky://quickpick/show",
209
210	// --- Source Control ---
211	SCMGroupChanged                               => "sky://scm/group/changed",
212
213	SCMProviderAdded                              => "sky://scm/provider/added",
214
215	SCMProviderChanged                            => "sky://scm/provider/changed",
216
217	SCMProviderRemoved                            => "sky://scm/provider/removed",
218
219	SCMRegister                                   => "sky://scm/register",
220
221	SCMUpdateGroup                                => "sky://scm/updateGroup",
222
223	// --- Status bar ---
224	// Canonical prefix is `sky://statusbar/` (no hyphen). The earlier
225	// `sky://status-bar/message` channel was an accidental fork produced by
226	// a separate emit site and has been consolidated onto
227	// `sky://statusbar/set-message`.
228	StatusBarCreate                               => "sky://statusbar/create",
229
230	StatusBarDispose                              => "sky://statusbar/dispose",
231
232	StatusBarDisposeEntry                         => "sky://statusbar/dispose-entry",
233
234	StatusBarDisposeMessage                       => "sky://statusbar/dispose-message",
235
236	StatusBarSetEntry                             => "sky://statusbar/set-entry",
237
238	StatusBarSetMessage                           => "sky://statusbar/set-message",
239
240	StatusBarUpdate                               => "sky://statusbar/update",
241
242	// --- Task ---
243	TaskExecute                                   => "sky://task/execute",
244
245	TaskTerminate                                 => "sky://task/terminate",
246
247	// --- Terminal ---
248	TerminalClosed                                => "sky://terminal/closed",
249
250	TerminalCreate                                => "sky://terminal/create",
251
252	TerminalData                                  => "sky://terminal/data",
253
254	TerminalExit                                  => "sky://terminal/exit",
255
256	TerminalHide                                  => "sky://terminal/hide",
257
258	TerminalOpened                                => "sky://terminal/opened",
259
260	TerminalProcessId                             => "sky://terminal/processId",
261
262	TerminalResize                                => "sky://terminal/resize",
263
264	TerminalShow                                  => "sky://terminal/show",
265
266	// --- Test ---
267	TestRegistered                                => "sky://test/registered",
268
269	TestRunStarted                                => "sky://test/run-started",
270
271	TestRunStatusChanged                          => "sky://test/run-status-changed",
272
273	// --- Theme ---
274	ThemeChange                                   => "sky://theme/change",
275
276	// --- Tree view ---
277	// Canonical prefix is `sky://tree-view/` (kebab-case). The earlier
278	// `sky://treeView/register` camelCase channel was a parallel emission
279	// from `CocoonService/TreeView.rs`; it has been collapsed into
280	// `TreeViewCreate`, which every handler already subscribes to.
281	TreeViewCreate                                => "sky://tree-view/create",
282
283	TreeViewDispose                               => "sky://tree-view/dispose",
284
285	TreeViewNodeExpanded                          => "sky://tree-view/node-expanded",
286
287	TreeViewRefresh                               => "sky://tree-view/refresh",
288
289	TreeViewRestoreState                          => "sky://tree-view/restore-state",
290
291	TreeViewReveal                                => "sky://tree-view/reveal",
292
293	TreeViewSelectionChanged                      => "sky://tree-view/selection-changed",
294
295	TreeViewSetBadge                              => "sky://tree-view/set-badge",
296
297	TreeViewSetMessage                            => "sky://tree-view/set-message",
298
299	TreeViewSetTitle                              => "sky://tree-view/set-title",
300
301	// --- UI ---
302	// `UIShow{InputBox,QuickPick}Request` are deprecated aliases. The
303	// Sky listener channels are `InputBoxShow` and `QuickPickShow`
304	// declared earlier in this enum. `UserInterfaceProvider.rs` now
305	// references those directly so the `UIShow*Request` channel names
306	// below remain reachable only from older code paths and tests.
307	UIShowInputBoxRequest                         => "sky://ui/show-input-box-request",
308
309	UIShowMessageRequest                          => "sky://ui/show-message-request",
310
311	UIShowQuickPickRequest                        => "sky://ui/show-quick-pick-request",
312
313	// --- Virtual file system ---
314	VFSFileChange                                 => "sky://vfs/fileChange",
315
316	// --- Webview ---
317	// Canonical form is kebab-case (`sky://webview/post-message`,
318	// `sky://webview/set-html`). The `…CamelCase` aliases existed because
319	// mod.rs emitted `sky://webview/postMessage` / `sky://webview/setHtml`
320	// inline; those emit sites have been migrated to the enum so Sky only
321	// ever sees the kebab-case form.
322	WebviewCreate                                 => "sky://webview/create",
323
324	WebviewCreated                                => "sky://webview/created",
325
326	WebviewDispose                                => "sky://webview/dispose",
327
328	WebviewDisposed                               => "sky://webview/disposed",
329
330	WebviewMessage                                => "sky://webview/message",
331
332	WebviewOptionsChanged                         => "sky://webview/options-changed",
333
334	WebviewPostMessage                            => "sky://webview/post-message",
335
336	WebviewRevealed                               => "sky://webview/revealed",
337
338	WebviewSetHTML                                => "sky://webview/set-html",
339
340	// --- Window ---
341	WindowShowTextDocument                        => "sky://window/showTextDocument",
342
343	// --- Workspace ---
344	WorkspaceApplyEdit                            => "sky://workspace/applyEdit",
345
346	WorkspacesChanged                             => "sky://workspaces/changed",
347}
348
349#[cfg(test)]
350mod Tests {
351
352	use std::str::FromStr;
353
354	use super::SkyEvent;
355
356	#[test]
357	fn RoundTrip() {
358		for Variant in SkyEvent::All() {
359			let Wire = Variant.AsStr();
360
361			let Parsed = SkyEvent::from_str(Wire).expect("round-trip");
362
363			assert_eq!(*Variant, Parsed, "{} failed round-trip", Wire);
364		}
365	}
366
367	#[test]
368	fn EveryWireStartsWithSkyScheme() {
369		for Variant in SkyEvent::All() {
370			assert!(
371				Variant.AsStr().starts_with("sky://"),
372				"{} does not use the sky:// scheme",
373				Variant.AsStr()
374			);
375		}
376	}
377
378	#[test]
379	fn RejectsUnknown() {
380		assert!(SkyEvent::from_str("mountain://nope").is_err());
381
382		assert!(SkyEvent::from_str("").is_err());
383	}
384
385	/// Guards against drift between this Rust enum and its TS mirror at
386	/// `Element/Wind/Source/IPC/SkyEvent.ts`. Both files are hand-edited,
387	/// so the test scrapes the TS literal array and asserts every wire
388	/// string here exists there, and vice versa. If this fails the two
389	/// tables disagree - add or remove from whichever side is missing.
390	#[test]
391	fn RustAndTypeScriptTablesAgree() {
392		use std::{collections::HashSet, path::PathBuf};
393
394		let TsPath = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../Wind/Source/IPC/SkyEvent.ts");
395
396		let Source = match std::fs::read_to_string(&TsPath) {
397			Ok(S) => S,
398
399			// In packaging contexts where Wind isn't checked out alongside
400			// Common we skip the cross-check silently rather than
401			// failing - the RoundTrip / UniqueWireStrings guards above
402			// still cover the Rust side on its own.
403			Err(_) => return,
404		};
405
406		let mut TsWires:HashSet<String> = HashSet::new();
407
408		for Line in Source.lines() {
409			if let Some(Start) = Line.find("\"sky://") {
410				let Tail = &Line[Start + 1..];
411
412				if let Some(End) = Tail.find('"') {
413					TsWires.insert(Tail[..End].to_string());
414				}
415			}
416		}
417
418		let RsWires:HashSet<String> = SkyEvent::All().iter().map(|V| V.AsStr().to_string()).collect();
419
420		let OnlyInRust:Vec<_> = RsWires.difference(&TsWires).collect();
421
422		let OnlyInTs:Vec<_> = TsWires.difference(&RsWires).collect();
423
424		assert!(
425			OnlyInRust.is_empty() && OnlyInTs.is_empty(),
426			"SkyEvent drift between Rust and TS:\n  only in Rust: {:?}\n  only in TS:   {:?}",
427			OnlyInRust,
428			OnlyInTs
429		);
430	}
431
432	#[test]
433	fn UniqueWireStrings() {
434		let mut Seen = std::collections::HashSet::new();
435
436		for Variant in SkyEvent::All() {
437			assert!(Seen.insert(Variant.AsStr()), "duplicate wire: {}", Variant.AsStr());
438		}
439	}
440}