use std::time::{SystemTime, UNIX_EPOCH};
use {
reovim_driver_session::{api::StateChanges, bridges::BridgeRegistry},
reovim_protocol::v2::{
BufferListChangedPayload, BufferModifiedPayload, CursorMovedPayload,
ExtensionUpdatedPayload, LayoutChangedPayload, ModeChangedPayload, Notification,
OptionChangedPayload, Position, SelectionChangedPayload, TabPageInfo,
ViewportUpdatedPayload, WindowInfo, WindowRect, notification,
},
};
use crate::session::{ClientId, Session};
#[allow(clippy::cast_possible_truncation)]
fn current_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before UNIX_EPOCH")
.as_millis() as u64
}
#[must_use]
pub fn build_notifications(
changes: &StateChanges,
session: &Session,
client_id: u64,
bridges: Option<&BridgeRegistry>,
) -> Vec<Notification> {
let mut notifications = Vec::new();
let timestamp = current_timestamp_ms();
if changes.mode_changed {
notifications.push(build_mode_notification(session, timestamp, client_id));
}
if changes.cursor_moved {
for buffer_id in &changes.affected_buffers {
if let Some(notification) =
build_cursor_notification(session, *buffer_id, timestamp, client_id)
{
notifications.push(notification);
}
}
}
if changes.buffer_modified {
for buffer_id in &changes.modified_buffers {
notifications.push(build_buffer_modified_notification(*buffer_id, timestamp));
}
}
for buffer_id in &changes.buffers_created {
notifications.push(build_buffer_list_notification("added", *buffer_id, timestamp));
}
for buffer_id in &changes.buffers_deleted {
notifications.push(build_buffer_list_notification("removed", *buffer_id, timestamp));
}
if changes.window_changed || changes.focus_changed {
notifications.push(build_layout_notification(session, timestamp, client_id));
}
if changes.selection_changed {
for buffer_id in &changes.affected_buffers {
if let Some(notification) =
build_selection_notification(session, *buffer_id, timestamp, client_id)
{
notifications.push(notification);
}
}
}
for opt_change in &changes.options_changed {
notifications.push(build_option_notification(opt_change, timestamp));
}
if changes.scroll_changed {
for window_id in &changes.scrolled_windows {
if let Some(notification) =
build_viewport_notification(session, *window_id, timestamp, client_id)
{
notifications.push(notification);
}
}
}
if changes.presence_changed {
for &cid in &changes.presence_updates {
if let Some(presence) = session.presence().get(ClientId::new(cid)) {
notifications.push(super::presence::build_presence_updated_notification(&presence));
}
}
}
if changes.extension_changed
&& let Some(registry) = bridges
{
for kind in &changes.extensions_updated {
if let Some(notification) =
build_extension_notification(kind, session, timestamp, client_id, registry)
{
notifications.push(notification);
}
}
}
notifications
}
#[allow(clippy::cast_possible_truncation)] fn build_mode_notification(session: &Session, timestamp: u64, client_id: u64) -> Notification {
let mode_name = session
.client_state(ClientId::new(client_id as usize))
.map_or_else(
|| {
tracing::warn!(%client_id, "Client not found for mode notification, using home_mode");
session.with_state_sync(|s| s.home_mode().name().to_string())
},
|s| s.mode_stack.current().name().to_string(),
);
let display_name = mode_name.to_uppercase();
let is_insert = mode_name.contains("insert") || mode_name.contains("replace");
Notification {
event_type: "mode_changed".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::ModeChanged(ModeChangedPayload {
name: mode_name,
display: display_name,
is_insert,
client_id,
})),
}
}
#[allow(clippy::cast_possible_truncation)] fn build_cursor_notification(
session: &Session,
buffer_id: reovim_kernel::api::v1::BufferId,
timestamp: u64,
client_id: u64,
) -> Option<Notification> {
let editing_state = session.client_state(ClientId::new(client_id as usize))?;
let active_id = editing_state.windows.active_id();
let window = editing_state
.windows
.windows
.iter()
.find(|w| w.buffer_id == Some(buffer_id) && Some(w.id) == active_id)
.or_else(|| {
editing_state
.windows
.windows
.iter()
.find(|w| w.buffer_id == Some(buffer_id))
})?;
Some(Notification {
event_type: "cursor_moved".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::CursorMoved(CursorMovedPayload {
window_id: window.id.as_usize() as u64,
position: Some(Position {
line: window.cursor.line as u64,
column: window.cursor.column as u64,
}),
client_id,
})),
})
}
fn build_buffer_modified_notification(
buffer_id: reovim_kernel::api::v1::BufferId,
timestamp: u64,
) -> Notification {
Notification {
event_type: "buffer_modified".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::BufferModified(BufferModifiedPayload {
buffer_id: buffer_id.as_usize() as u64,
change: None, })),
}
}
fn build_buffer_list_notification(
action: &str,
buffer_id: reovim_kernel::api::v1::BufferId,
timestamp: u64,
) -> Notification {
Notification {
event_type: "buffer_list_changed".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::BufferListChanged(BufferListChangedPayload {
action: action.to_string(),
buffer_id: buffer_id.as_usize() as u64,
})),
}
}
#[allow(clippy::cast_possible_truncation, clippy::option_if_let_else)]
fn build_layout_notification(session: &Session, timestamp: u64, client_id: u64) -> Notification {
let editing_state = session.client_state(ClientId::new(client_id as usize));
let focused_id = editing_state.as_ref().and_then(|s| s.windows.active_id());
let windows: Vec<WindowInfo> = if let Some(ref state) = editing_state {
if let Some(ref compositor) = state.compositor {
let (tw, th) = state.terminal_size;
let screen = reovim_driver_layout::Rect::new(0, 0, tw, th);
let composite = compositor.composite(screen);
let active_buffer = state.active_buffer;
composite
.placements
.iter()
.map(|p| {
let buffer_id = state
.windows
.get(p.window_id)
.and_then(|w| w.buffer_id)
.or(active_buffer)
.map(|id| id.as_usize() as u64);
WindowInfo {
window_id: p.window_id.as_usize() as u64,
buffer_id,
rect: Some(WindowRect {
x: u64::from(p.bounds.x),
y: u64::from(p.bounds.y),
width: u64::from(p.bounds.width),
height: u64::from(p.bounds.height),
}),
focused: focused_id == Some(p.window_id),
opacity: Some(p.opacity),
}
})
.collect()
} else {
state
.windows
.windows
.iter()
.map(|w| WindowInfo {
window_id: w.id.as_usize() as u64,
buffer_id: w.buffer_id.map(|id| id.as_usize() as u64),
rect: Some(WindowRect {
x: 0,
y: 0,
width: 80,
height: 24,
}),
focused: focused_id == Some(w.id),
opacity: None, })
.collect()
}
} else {
Vec::new()
};
let (active_tab_id, tabs_info) = if let Some(ref state) = editing_state {
let tab_set = &state.tabs;
let active_id = Some(tab_set.active_tab_id().as_usize() as u64);
let info: Vec<TabPageInfo> = tab_set
.tab_info()
.into_iter()
.map(|(id, label, is_active)| TabPageInfo {
tab_id: id.as_usize() as u64,
label: label.to_string(),
active: is_active,
})
.collect();
(active_id, info)
} else {
(None, Vec::new())
};
Notification {
event_type: "layout_changed".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::LayoutChanged(LayoutChangedPayload {
focused_window_id: focused_id.map(|id| id.as_usize() as u64),
windows,
client_id,
active_tab_id,
tabs: tabs_info,
})),
}
}
#[allow(clippy::cast_possible_truncation, clippy::significant_drop_tightening)]
fn build_selection_notification(
session: &Session,
buffer_id: reovim_kernel::api::v1::BufferId,
timestamp: u64,
client_id: u64,
) -> Option<Notification> {
let editing_state = session.client_state(ClientId::new(client_id as usize))?;
let window = editing_state
.windows
.windows
.iter()
.find(|w| w.buffer_id == Some(buffer_id))?;
let (has_selection, selection, visual_mode) =
window
.selection
.as_ref()
.map_or((false, None, None), |sel| {
use reovim_driver_session::SelectionMode;
let mode_str = match sel.mode {
SelectionMode::Character => "char",
SelectionMode::Line => "line",
SelectionMode::Block => "block",
};
(
true,
Some(reovim_protocol::v2::Selection {
start: Some(Position {
line: sel.start.line as u64,
column: sel.start.column as u64,
}),
end: Some(Position {
line: sel.end.line as u64,
column: sel.end.column as u64,
}),
}),
Some(mode_str.to_string()),
)
});
Some(Notification {
event_type: "selection_changed".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::SelectionChanged(SelectionChangedPayload {
window_id: window.id.as_usize() as u64,
has_selection,
selection,
visual_mode,
client_id,
})),
})
}
#[allow(clippy::cast_possible_truncation)]
fn build_viewport_notification(
session: &Session,
window_id: reovim_kernel::api::v1::WindowId,
timestamp: u64,
client_id: u64,
) -> Option<Notification> {
let editing_state = session.client_state(ClientId::new(client_id as usize))?;
let window = editing_state.windows.get(window_id)?;
Some(Notification {
event_type: "viewport_updated".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::ViewportUpdated(ViewportUpdatedPayload {
viewport_id: window_id.as_usize() as u64,
top_line: Some(window.viewport.scroll_top as u32),
left_col: Some(window.viewport.scroll_left as u32),
cursor_line: Some(window.cursor.line as u32),
cursor_col: Some(window.cursor.column as u32),
})),
})
}
fn build_option_notification(
opt_change: &reovim_driver_session::api::OptionChange,
timestamp: u64,
) -> Notification {
use {reovim_kernel::api::v1::OptionValue, reovim_protocol::v2::option_changed_payload::Value};
let value = match &opt_change.value {
OptionValue::Bool(b) => Some(Value::BoolValue(*b)),
OptionValue::Integer(i) => Some(Value::IntValue(*i)),
OptionValue::String(s) => Some(Value::StringValue(s.clone())),
OptionValue::Choice { value, .. } => Some(Value::StringValue(value.clone())),
};
Notification {
event_type: "option_changed".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::OptionChanged(OptionChangedPayload {
name: opt_change.name.clone(),
value,
})),
}
}
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn build_extension_notification(
kind: &str,
session: &Session,
timestamp: u64,
client_id: u64,
bridges: &BridgeRegistry,
) -> Option<Notification> {
use reovim_driver_session::bridges::{BridgeContext, ExtensionScope};
let bridge = bridges.get(kind)?;
let cid = ClientId::new(client_id as usize);
let data = match bridge.scope() {
ExtensionScope::Client => {
session.with_bridge_context(cid, |own_ext, shared_ext, opponents| {
let driver_cid = reovim_driver_session::ClientId::new(client_id as usize);
let context = BridgeContext::new(driver_cid, shared_ext, opponents);
bridge.snapshot_with_context(own_ext, &context)
})??
}
ExtensionScope::Shared => {
session.with_state_sync(|state| bridge.snapshot(&state.app.extensions))?
}
};
Some(Notification {
event_type: "extension_updated".to_string(),
timestamp_ms: timestamp,
payload: Some(notification::Payload::ExtensionUpdated(ExtensionUpdatedPayload {
kind: kind.to_string(),
data: data.to_string(),
client_id,
})),
})
}
#[cfg(test)]
#[path = "notification_builder_tests.rs"]
mod tests;