1pub use reovim_driver_syntax::SyntaxSessionState;
20
21use {
22 reovim_driver_session::SessionExtension,
23 reovim_driver_syntax::SyntaxEdit,
24 reovim_kernel::api::v1::BufferId,
25 reovim_protocol::v2::{TokenSpan, TokenUpdate},
26 tokio::sync::mpsc,
27};
28
29pub type TokenSubscriber = mpsc::Sender<TokenUpdate>;
31
32#[derive(Default)]
43pub struct SyntaxStreamState {
44 subscribers: Vec<TokenSubscriber>,
46}
47
48impl SessionExtension for SyntaxStreamState {
49 fn create() -> Self {
50 Self::default()
51 }
52}
53
54impl SyntaxStreamState {
55 #[must_use]
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 #[must_use]
71 pub fn subscribe(&mut self) -> mpsc::Receiver<TokenUpdate> {
72 let (tx, rx) = mpsc::channel(16);
73 self.subscribers.push(tx);
74 rx
75 }
76
77 #[must_use]
79 pub const fn subscriber_count(&self) -> usize {
80 self.subscribers.len()
81 }
82
83 #[must_use]
85 pub const fn has_subscribers(&self) -> bool {
86 !self.subscribers.is_empty()
87 }
88
89 pub fn broadcast(&mut self, update: &TokenUpdate) {
93 self.subscribers
94 .retain(|tx| tx.try_send(update.clone()).is_ok());
95 }
96
97 #[allow(clippy::cast_possible_truncation)]
113 pub fn notify_edit(
114 &mut self,
115 syntax: &mut SyntaxSessionState,
116 buffer_id: BufferId,
117 content: &str,
118 edit: &SyntaxEdit,
119 start_line: u64,
120 end_line: u64,
121 ) {
122 let Some(driver) = syntax.get_mut(buffer_id) else {
124 return; };
126
127 driver.update(content, edit);
129
130 if self.subscribers.is_empty() {
132 return;
133 }
134
135 let start_byte = edit.start_byte.saturating_sub(100);
138 let end_byte = (edit.new_end_byte + 100).min(content.len());
139
140 let Some(driver) = syntax.get(buffer_id) else {
142 return;
143 };
144 let mut highlights = driver.highlights(start_byte..end_byte);
145 highlights.extend(driver.decorations(start_byte..end_byte));
146
147 let tokens: Vec<TokenSpan> = highlights
149 .into_iter()
150 .map(|span| TokenSpan {
151 start_byte: span.start_byte as u32,
152 end_byte: span.end_byte as u32,
153 category: span.category.to_string(),
154 })
155 .collect();
156
157 let update = TokenUpdate {
159 buffer_id: buffer_id.as_usize() as u64,
160 tokens,
161 start_line,
162 end_line,
163 full_refresh: false,
164 layer: "syntax".into(),
165 priority: 0,
166 };
167
168 self.subscribers
170 .retain(|tx| tx.try_send(update.clone()).is_ok());
171 }
172
173 #[allow(clippy::cast_possible_truncation)]
177 pub fn send_full_refresh(
178 &mut self,
179 syntax: &SyntaxSessionState,
180 buffer_id: BufferId,
181 total_lines: u64,
182 ) {
183 let Some(driver) = syntax.get(buffer_id) else {
184 return;
185 };
186
187 if self.subscribers.is_empty() {
188 return;
189 }
190
191 let mut highlights = driver.highlights(0..usize::MAX);
193 highlights.extend(driver.decorations(0..usize::MAX));
194
195 let tokens: Vec<TokenSpan> = highlights
197 .into_iter()
198 .map(|span| TokenSpan {
199 start_byte: span.start_byte as u32,
200 end_byte: span.end_byte as u32,
201 category: span.category.to_string(),
202 })
203 .collect();
204
205 let update = TokenUpdate {
206 buffer_id: buffer_id.as_usize() as u64,
207 tokens,
208 start_line: 0,
209 end_line: total_lines.saturating_sub(1),
210 full_refresh: true,
211 layer: "syntax".into(),
212 priority: 0,
213 };
214
215 self.subscribers
217 .retain(|tx| tx.try_send(update.clone()).is_ok());
218 }
219}
220
221#[must_use]
225pub fn compute_end_position(start_row: u32, start_col: u32, text: &str) -> (u32, u32) {
226 let mut row = start_row;
227 let mut col = start_col;
228 for ch in text.chars() {
229 if ch == '\n' {
230 row += 1;
231 col = 0;
232 } else {
233 col += 1;
234 }
235 }
236 (row, col)
237}
238
239#[must_use]
247pub fn modification_to_syntax_edit(
248 modification: &reovim_kernel::api::v1::events::kernel::Modification,
249) -> Option<SyntaxEdit> {
250 use reovim_kernel::api::v1::events::kernel::Modification;
251
252 match modification {
253 Modification::Insert {
254 start,
255 text,
256 start_byte,
257 } => {
258 let new_end_byte = start_byte + text.len();
259 let (new_end_row, new_end_col) = compute_end_position(start.0, start.1, text);
260 Some(SyntaxEdit::insert(
261 *start_byte,
262 start.0,
263 start.1,
264 new_end_byte,
265 new_end_row,
266 new_end_col,
267 ))
268 }
269 Modification::Delete {
270 start,
271 end,
272 text,
273 start_byte,
274 } => {
275 let old_end_byte = start_byte + text.len();
276 Some(SyntaxEdit::delete(*start_byte, start.0, start.1, old_end_byte, end.0, end.1))
277 }
278 Modification::Replace {
279 start,
280 end,
281 old_text,
282 new_text,
283 start_byte,
284 } => {
285 let old_end_byte = start_byte + old_text.len();
286 let new_end_byte = start_byte + new_text.len();
287 let (new_end_row, new_end_col) = compute_end_position(start.0, start.1, new_text);
288 Some(SyntaxEdit::new(
289 *start_byte,
290 old_end_byte,
291 new_end_byte,
292 start.0,
293 start.1,
294 end.0,
295 end.1,
296 new_end_row,
297 new_end_col,
298 ))
299 }
300 Modification::FullReplace => None,
301 }
302}
303
304#[must_use]
311#[allow(clippy::cast_possible_truncation)]
312pub fn build_token_update(
313 syntax: &SyntaxSessionState,
314 buffer_id: BufferId,
315 total_lines: u64,
316 full_refresh: bool,
317) -> Option<TokenUpdate> {
318 let driver = syntax.get(buffer_id)?;
319 let mut highlights = driver.highlights(0..usize::MAX);
320 highlights.extend(driver.decorations(0..usize::MAX));
321
322 let tokens: Vec<TokenSpan> = highlights
323 .into_iter()
324 .map(|span| TokenSpan {
325 start_byte: span.start_byte as u32,
326 end_byte: span.end_byte as u32,
327 category: span.category.to_string(),
328 })
329 .collect();
330
331 Some(TokenUpdate {
332 buffer_id: buffer_id.as_usize() as u64,
333 tokens,
334 start_line: 0,
335 end_line: total_lines.saturating_sub(1),
336 full_refresh,
337 layer: "syntax".into(),
338 priority: 0,
339 })
340}
341
342impl std::fmt::Debug for SyntaxStreamState {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("SyntaxStreamState")
345 .field("subscriber_count", &self.subscribers.len())
346 .finish()
347 }
348}
349
350#[cfg(test)]
351#[path = "syntax_state_tests.rs"]
352mod tests;