pub use reovim_driver_syntax::SyntaxSessionState;
use {
reovim_driver_session::SessionExtension,
reovim_driver_syntax::SyntaxEdit,
reovim_kernel::api::v1::BufferId,
reovim_protocol::v2::{TokenSpan, TokenUpdate},
tokio::sync::mpsc,
};
pub type TokenSubscriber = mpsc::Sender<TokenUpdate>;
#[derive(Default)]
pub struct SyntaxStreamState {
subscribers: Vec<TokenSubscriber>,
}
impl SessionExtension for SyntaxStreamState {
fn create() -> Self {
Self::default()
}
}
impl SyntaxStreamState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn subscribe(&mut self) -> mpsc::Receiver<TokenUpdate> {
let (tx, rx) = mpsc::channel(16);
self.subscribers.push(tx);
rx
}
#[must_use]
pub const fn subscriber_count(&self) -> usize {
self.subscribers.len()
}
#[must_use]
pub const fn has_subscribers(&self) -> bool {
!self.subscribers.is_empty()
}
pub fn broadcast(&mut self, update: &TokenUpdate) {
self.subscribers
.retain(|tx| tx.try_send(update.clone()).is_ok());
}
#[allow(clippy::cast_possible_truncation)]
pub fn notify_edit(
&mut self,
syntax: &mut SyntaxSessionState,
buffer_id: BufferId,
content: &str,
edit: &SyntaxEdit,
start_line: u64,
end_line: u64,
) {
let Some(driver) = syntax.get_mut(buffer_id) else {
return; };
driver.update(content, edit);
if self.subscribers.is_empty() {
return;
}
let start_byte = edit.start_byte.saturating_sub(100);
let end_byte = (edit.new_end_byte + 100).min(content.len());
let Some(driver) = syntax.get(buffer_id) else {
return;
};
let mut highlights = driver.highlights(start_byte..end_byte);
highlights.extend(driver.decorations(start_byte..end_byte));
let tokens: Vec<TokenSpan> = highlights
.into_iter()
.map(|span| TokenSpan {
start_byte: span.start_byte as u32,
end_byte: span.end_byte as u32,
category: span.category.to_string(),
})
.collect();
let update = TokenUpdate {
buffer_id: buffer_id.as_usize() as u64,
tokens,
start_line,
end_line,
full_refresh: false,
layer: "syntax".into(),
priority: 0,
};
self.subscribers
.retain(|tx| tx.try_send(update.clone()).is_ok());
}
#[allow(clippy::cast_possible_truncation)]
pub fn send_full_refresh(
&mut self,
syntax: &SyntaxSessionState,
buffer_id: BufferId,
total_lines: u64,
) {
let Some(driver) = syntax.get(buffer_id) else {
return;
};
if self.subscribers.is_empty() {
return;
}
let mut highlights = driver.highlights(0..usize::MAX);
highlights.extend(driver.decorations(0..usize::MAX));
let tokens: Vec<TokenSpan> = highlights
.into_iter()
.map(|span| TokenSpan {
start_byte: span.start_byte as u32,
end_byte: span.end_byte as u32,
category: span.category.to_string(),
})
.collect();
let update = TokenUpdate {
buffer_id: buffer_id.as_usize() as u64,
tokens,
start_line: 0,
end_line: total_lines.saturating_sub(1),
full_refresh: true,
layer: "syntax".into(),
priority: 0,
};
self.subscribers
.retain(|tx| tx.try_send(update.clone()).is_ok());
}
}
#[must_use]
pub fn compute_end_position(start_row: u32, start_col: u32, text: &str) -> (u32, u32) {
let mut row = start_row;
let mut col = start_col;
for ch in text.chars() {
if ch == '\n' {
row += 1;
col = 0;
} else {
col += 1;
}
}
(row, col)
}
#[must_use]
pub fn modification_to_syntax_edit(
modification: &reovim_kernel::api::v1::events::kernel::Modification,
) -> Option<SyntaxEdit> {
use reovim_kernel::api::v1::events::kernel::Modification;
match modification {
Modification::Insert {
start,
text,
start_byte,
} => {
let new_end_byte = start_byte + text.len();
let (new_end_row, new_end_col) = compute_end_position(start.0, start.1, text);
Some(SyntaxEdit::insert(
*start_byte,
start.0,
start.1,
new_end_byte,
new_end_row,
new_end_col,
))
}
Modification::Delete {
start,
end,
text,
start_byte,
} => {
let old_end_byte = start_byte + text.len();
Some(SyntaxEdit::delete(*start_byte, start.0, start.1, old_end_byte, end.0, end.1))
}
Modification::Replace {
start,
end,
old_text,
new_text,
start_byte,
} => {
let old_end_byte = start_byte + old_text.len();
let new_end_byte = start_byte + new_text.len();
let (new_end_row, new_end_col) = compute_end_position(start.0, start.1, new_text);
Some(SyntaxEdit::new(
*start_byte,
old_end_byte,
new_end_byte,
start.0,
start.1,
end.0,
end.1,
new_end_row,
new_end_col,
))
}
Modification::FullReplace => None,
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn build_token_update(
syntax: &SyntaxSessionState,
buffer_id: BufferId,
total_lines: u64,
full_refresh: bool,
) -> Option<TokenUpdate> {
let driver = syntax.get(buffer_id)?;
let mut highlights = driver.highlights(0..usize::MAX);
highlights.extend(driver.decorations(0..usize::MAX));
let tokens: Vec<TokenSpan> = highlights
.into_iter()
.map(|span| TokenSpan {
start_byte: span.start_byte as u32,
end_byte: span.end_byte as u32,
category: span.category.to_string(),
})
.collect();
Some(TokenUpdate {
buffer_id: buffer_id.as_usize() as u64,
tokens,
start_line: 0,
end_line: total_lines.saturating_sub(1),
full_refresh,
layer: "syntax".into(),
priority: 0,
})
}
impl std::fmt::Debug for SyntaxStreamState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyntaxStreamState")
.field("subscriber_count", &self.subscribers.len())
.finish()
}
}
#[cfg(test)]
#[path = "syntax_state_tests.rs"]
mod tests;