reovim_driver_session/notification_queue.rs
1//! Thread-safe pending notification queue.
2//!
3//! Background threads (LSP completion, snippet reload) push notifications here.
4//! A synchronous command handler drains the queue into `NotificationState` on
5//! each invocation.
6//!
7//! This avoids the problem that background threads lack `SessionRuntime`
8//! access needed for `NotificationState`.
9//!
10//! # Architecture
11//!
12//! This lives in the driver layer (mechanism) because multiple modules
13//! need to push notifications without depending on each other:
14//! - `completion` module: LSP completion results
15//! - `snippet` module: reload/catalog notifications
16//! - `lsp` module: `$/progress`, `window/showMessage`
17//! - `notification` module: drains into display state
18
19use {parking_lot::Mutex, reovim_kernel::api::v1::Service};
20
21/// Notification level for pending notifications.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PendingLevel {
24 /// Informational message.
25 Info,
26 /// Success confirmation.
27 Success,
28 /// Warning message.
29 Warning,
30 /// Error message.
31 Error,
32}
33
34/// Operation type for the pending notification queue.
35///
36/// Extends the original flat push with progress lifecycle operations
37/// for `$/progress` and similar producer protocols (#691).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum PendingOp {
40 /// Simple notification (existing behavior).
41 Push {
42 /// Severity level.
43 level: PendingLevel,
44 /// Short title.
45 title: String,
46 },
47 /// Begin a progress notification.
48 ProgressBegin {
49 /// Opaque token from the producer (e.g., LSP progress token).
50 token: String,
51 /// Short title (e.g., "Indexing").
52 title: String,
53 /// Optional detail message.
54 message: String,
55 /// Initial percentage (0..=100), or 0 if indeterminate.
56 percentage: u8,
57 },
58 /// Update a progress notification.
59 ProgressReport {
60 /// Token matching a previous `ProgressBegin`.
61 token: String,
62 /// Updated detail message (if any).
63 message: Option<String>,
64 /// Updated percentage (if any).
65 percentage: Option<u8>,
66 },
67 /// End a progress notification.
68 ProgressEnd {
69 /// Token matching a previous `ProgressBegin`.
70 token: String,
71 /// Optional final message.
72 message: Option<String>,
73 },
74}
75
76/// A pending notification entry with optional source attribution.
77///
78/// Wraps a [`PendingOp`] with an optional source tag for producer grouping.
79/// The source identifies the notification producer (e.g., `"rust-analyzer"`,
80/// `"git"`) so the TUI can group notifications into bordered boxes (#691).
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PendingEntry {
83 /// Optional producer name for grouped display.
84 pub source: Option<String>,
85 /// The notification operation.
86 pub op: PendingOp,
87}
88
89/// A pending notification to be flushed to display state.
90///
91/// Retained for backward compatibility. New code should use
92/// [`PendingEntry`] + [`PendingOp`] via [`PendingNotificationQueue::push_op`].
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PendingNotification {
95 /// Severity level.
96 pub level: PendingLevel,
97 /// Short title.
98 pub title: String,
99}
100
101/// Thread-safe queue for notifications from background threads.
102///
103/// Registered as a [`Service`] in `ServiceRegistry` so that both the
104/// background threads (which have `Arc<ServiceRegistry>`) and the
105/// command handler (which has `SessionRuntime`) can access it.
106#[derive(Debug)]
107pub struct PendingNotificationQueue {
108 queue: Mutex<Vec<PendingEntry>>,
109}
110
111impl PendingNotificationQueue {
112 /// Create a new empty queue.
113 #[must_use]
114 pub const fn new() -> Self {
115 Self {
116 queue: Mutex::new(Vec::new()),
117 }
118 }
119
120 /// Push a simple notification from any thread.
121 ///
122 /// Convenience method for sourceless push operations.
123 /// For source-tagged or progress notifications, use [`push_op`](Self::push_op).
124 pub fn push(&self, level: PendingLevel, title: impl Into<String>) {
125 self.queue.lock().push(PendingEntry {
126 source: None,
127 op: PendingOp::Push {
128 level,
129 title: title.into(),
130 },
131 });
132 }
133
134 /// Push a notification operation with optional source attribution.
135 pub fn push_op(&self, source: Option<String>, op: PendingOp) {
136 self.queue.lock().push(PendingEntry { source, op });
137 }
138
139 /// Drain all pending entries.
140 ///
141 /// Called from the command handler context where `SessionRuntime`
142 /// is available to forward them to the display system.
143 pub fn drain(&self) -> Vec<PendingEntry> {
144 let mut queue = self.queue.lock();
145 std::mem::take(&mut *queue)
146 }
147
148 /// Number of pending entries.
149 #[must_use]
150 pub fn len(&self) -> usize {
151 self.queue.lock().len()
152 }
153
154 /// Whether the queue is empty.
155 #[must_use]
156 pub fn is_empty(&self) -> bool {
157 self.queue.lock().is_empty()
158 }
159}
160
161impl Default for PendingNotificationQueue {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl Service for PendingNotificationQueue {}
168#[cfg(test)]
169#[path = "notification_queue_tests.rs"]
170mod tests;