use std::{collections::HashMap, time::SystemTime};
use {
reovim_driver_layout::RootCompositor,
reovim_driver_session::{
CursorPosition, ExtensionMap, KeySequence, Selection, SelectionMode, TabPageSet, Viewport,
Window, WindowLayout,
},
reovim_kernel::api::v1::{BufferId, HistoryRing, Jumplist, MarkBank, ModeStack, RegisterBank},
};
use super::{ClientId, ring_buffer::ClientRingBuffer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientRelation {
Following {
target: ClientId,
},
Sharing {
with: ClientId,
},
}
impl ClientRelation {
#[must_use]
pub const fn target_id(&self) -> ClientId {
match *self {
Self::Following { target } => target,
Self::Sharing { with } => with,
}
}
#[must_use]
pub const fn is_following(&self) -> bool {
matches!(self, Self::Following { .. })
}
#[must_use]
pub const fn is_sharing(&self) -> bool {
matches!(self, Self::Sharing { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransitionResult {
Ok,
RequiresCursorSync {
current: CursorPosition,
target: CursorPosition,
},
TargetNotFound(ClientId),
WouldCreateCycle,
CannotTargetSelf,
}
impl TransitionResult {
#[must_use]
pub const fn is_ok(&self) -> bool {
matches!(self, Self::Ok)
}
#[must_use]
pub const fn requires_cursor_sync(&self) -> bool {
matches!(self, Self::RequiresCursorSync { .. })
}
}
fn would_create_cycle(
start: ClientId,
target: ClientId,
clients: &HashMap<ClientId, Client>,
) -> bool {
would_create_cycle_impl(start, target, clients, 10)
}
fn would_create_cycle_impl(
start: ClientId,
target: ClientId,
clients: &HashMap<ClientId, Client>,
depth: usize,
) -> bool {
if depth == 0 {
return false; }
let Some(target_client) = clients.get(&target) else {
return false;
};
match target_client.relation {
Some(
ClientRelation::Following { target: next } | ClientRelation::Sharing { with: next },
) => {
if next == start {
return true;
}
would_create_cycle_impl(start, next, clients, depth - 1)
}
None => false,
}
}
#[derive(Debug, Clone)]
pub struct ClientMetadata {
pub client_type: String,
pub display_name: String,
pub joined_at_ms: u64,
}
impl ClientMetadata {
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn new(client_type: impl Into<String>, display_name: impl Into<String>) -> Self {
Self {
client_type: client_type.into(),
display_name: display_name.into(),
joined_at_ms: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_millis() as u64),
}
}
#[must_use]
pub fn with_timestamp(
client_type: impl Into<String>,
display_name: impl Into<String>,
joined_at_ms: u64,
) -> Self {
Self {
client_type: client_type.into(),
display_name: display_name.into(),
joined_at_ms,
}
}
}
impl Default for ClientMetadata {
fn default() -> Self {
Self::new("unknown", "unknown")
}
}
#[derive(Debug, Clone)]
pub struct Client {
pub id: ClientId,
pub relation: Option<ClientRelation>,
pub state: EditingState,
pub metadata: ClientMetadata,
pub ring_buffer: ClientRingBuffer,
}
impl Client {
#[must_use]
pub fn new(id: ClientId, metadata: ClientMetadata) -> Self {
Self {
id,
relation: None,
state: EditingState::default(),
metadata,
ring_buffer: ClientRingBuffer::new(),
}
}
#[must_use]
pub fn with_mode_stack(id: ClientId, metadata: ClientMetadata, mode_stack: ModeStack) -> Self {
Self {
id,
relation: None,
state: EditingState::with_mode_stack(mode_stack),
metadata,
ring_buffer: ClientRingBuffer::new(),
}
}
#[must_use]
pub fn with_mode_stack_and_window(
id: ClientId,
metadata: ClientMetadata,
mode_stack: ModeStack,
window: Window,
) -> Self {
Self {
id,
relation: None,
state: EditingState::with_mode_stack_and_window(mode_stack, window),
metadata,
ring_buffer: ClientRingBuffer::new(),
}
}
#[must_use]
pub const fn ring_buffer(&self) -> &ClientRingBuffer {
&self.ring_buffer
}
#[must_use]
pub const fn is_independent(&self) -> bool {
self.relation.is_none()
}
#[must_use]
pub const fn is_following(&self) -> bool {
matches!(self.relation, Some(ClientRelation::Following { .. }))
}
#[must_use]
pub const fn is_sharing(&self) -> bool {
matches!(self.relation, Some(ClientRelation::Sharing { .. }))
}
#[must_use]
pub const fn target_id(&self) -> Option<ClientId> {
match self.relation {
Some(ClientRelation::Following { target }) => Some(target),
Some(ClientRelation::Sharing { with }) => Some(with),
None => None,
}
}
#[must_use]
pub fn validate_relation_change(
client: &Self,
new_relation: Option<ClientRelation>,
clients: &HashMap<ClientId, Self>,
) -> TransitionResult {
if let Some(
ClientRelation::Following { target } | ClientRelation::Sharing { with: target },
) = new_relation
{
if target == client.id {
return TransitionResult::CannotTargetSelf;
}
let Some(target_client) = clients.get(&target) else {
return TransitionResult::TargetNotFound(target);
};
if would_create_cycle(client.id, target, clients) {
return TransitionResult::WouldCreateCycle;
}
if let (
Some(ClientRelation::Following { target: old_target }),
Some(ClientRelation::Sharing { with: new_target }),
) = (&client.relation, &new_relation)
{
if old_target == new_target {
let target_cursor = target_client
.state
.windows
.active()
.map_or_else(CursorPosition::default, |w| w.cursor);
let my_cursor = client
.state
.windows
.active()
.map_or_else(CursorPosition::default, |w| w.cursor);
if my_cursor != target_cursor {
return TransitionResult::RequiresCursorSync {
current: my_cursor,
target: target_cursor,
};
}
}
}
}
TransitionResult::Ok
}
pub fn try_set_relation(
&mut self,
new_relation: Option<ClientRelation>,
clients: &HashMap<ClientId, Self>,
) -> TransitionResult {
let result = Self::validate_relation_change(self, new_relation, clients);
if result.is_ok() {
self.relation = new_relation;
}
result
}
pub fn sync_cursor_to(&mut self, target: &Self) {
if let (Some(target_window), Some(my_window)) =
(target.state.windows.active(), self.state.windows.active_mut())
{
my_window.cursor = target_window.cursor;
}
}
pub const fn set_relation_unchecked(&mut self, relation: Option<ClientRelation>) {
self.relation = relation;
}
#[must_use]
pub fn effective_state<'a>(
&'a self,
clients: &'a HashMap<ClientId, Self>,
) -> Option<&'a EditingState> {
match self.relation {
None => Some(&self.state),
Some(
ClientRelation::Following { target } | ClientRelation::Sharing { with: target },
) => Self::resolve_state(target, clients, 10),
}
}
pub fn effective_state_mut<'a>(
&'a self,
clients: &'a mut HashMap<ClientId, Self>,
) -> Option<&'a mut EditingState> {
match self.relation {
None => {
None
}
Some(ClientRelation::Following { .. }) => None, Some(ClientRelation::Sharing { with }) => clients.get_mut(&with).map(|c| &mut c.state),
}
}
fn resolve_state(
target: ClientId,
clients: &HashMap<ClientId, Self>,
depth: usize,
) -> Option<&EditingState> {
if depth == 0 {
return None; }
let client = clients.get(&target)?;
match client.relation {
None => Some(&client.state),
Some(
ClientRelation::Following { target: next } | ClientRelation::Sharing { with: next },
) => Self::resolve_state(next, clients, depth - 1),
}
}
}
pub struct EditingState {
pub mode_stack: ModeStack,
pub pending_keys: KeySequence,
pub windows: WindowLayout,
pub viewport: Viewport,
pub selection: Option<ClientSelection>,
pub extensions: ExtensionMap,
pub compositor: Option<Box<dyn RootCompositor>>,
pub tabs: TabPageSet,
pub registers: RegisterBank,
pub clipboard_history: HistoryRing,
pub local_marks: MarkBank,
pub jumplist: Jumplist,
pub active_buffer: Option<BufferId>,
pub terminal_size: (u16, u16),
}
impl std::fmt::Debug for EditingState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditingState")
.field("mode_stack", &self.mode_stack)
.field("pending_keys", &self.pending_keys)
.field("windows", &self.windows)
.field("viewport", &self.viewport)
.field("selection", &self.selection)
.field("extensions", &self.extensions)
.field("compositor", &self.compositor.as_ref().map(|_| "..."))
.field("tabs", &self.tabs)
.field("registers", &self.registers)
.field("clipboard_history", &self.clipboard_history)
.field("local_marks", &self.local_marks)
.field("jumplist", &self.jumplist)
.field("active_buffer", &self.active_buffer)
.field("terminal_size", &self.terminal_size)
.finish()
}
}
impl Clone for EditingState {
fn clone(&self) -> Self {
Self {
mode_stack: self.mode_stack.clone(),
pending_keys: self.pending_keys.clone(),
windows: self.windows.clone(),
viewport: self.viewport, selection: self.selection.clone(),
extensions: ExtensionMap::new(), compositor: self.compositor.as_ref().map(|c| c.boxed_clone()), tabs: self.tabs.clone(), registers: self.registers.clone(), clipboard_history: self.clipboard_history.clone(), local_marks: self.local_marks.clone(), jumplist: self.jumplist.clone(), active_buffer: self.active_buffer, terminal_size: self.terminal_size, }
}
}
impl Default for EditingState {
fn default() -> Self {
let placeholder_mode = reovim_kernel::api::v1::ModeId::new(
reovim_kernel::api::v1::ModuleId::new("default"),
"normal",
);
Self {
mode_stack: ModeStack::new(placeholder_mode),
pending_keys: KeySequence::new(),
windows: WindowLayout::empty(),
viewport: Viewport::default(),
selection: None,
extensions: ExtensionMap::new(),
compositor: None,
tabs: TabPageSet::new(),
registers: RegisterBank::new(),
clipboard_history: HistoryRing::new(),
local_marks: MarkBank::new(),
jumplist: Jumplist::new(),
active_buffer: None,
terminal_size: (80, 24),
}
}
}
impl EditingState {
#[must_use]
pub fn with_mode_stack(mode_stack: ModeStack) -> Self {
Self {
mode_stack,
pending_keys: KeySequence::new(),
windows: WindowLayout::empty(),
viewport: Viewport::default(),
selection: None,
extensions: ExtensionMap::new(),
compositor: None,
tabs: TabPageSet::new(),
registers: RegisterBank::new(),
clipboard_history: HistoryRing::new(),
local_marks: MarkBank::new(),
jumplist: Jumplist::new(),
active_buffer: None,
terminal_size: (80, 24),
}
}
#[must_use]
pub fn with_mode_stack_and_window(mode_stack: ModeStack, window: Window) -> Self {
let mut windows = WindowLayout::empty();
windows.add(window);
Self {
mode_stack,
pending_keys: KeySequence::new(),
windows,
viewport: Viewport::default(),
selection: None,
extensions: ExtensionMap::new(),
compositor: None,
tabs: TabPageSet::new(),
registers: RegisterBank::new(),
clipboard_history: HistoryRing::new(),
local_marks: MarkBank::new(),
jumplist: Jumplist::new(),
active_buffer: None,
terminal_size: (80, 24),
}
}
#[must_use]
pub fn current_mode(&self) -> &reovim_kernel::api::v1::ModeId {
self.mode_stack.current()
}
pub fn clear_pending_keys(&mut self) {
self.pending_keys.clear();
}
pub fn client_context(&mut self) -> reovim_driver_session::ClientContext<'_> {
reovim_driver_session::ClientContext {
mode_stack: &mut self.mode_stack,
windows: &mut self.windows,
extensions: &mut self.extensions,
compositor: &mut self.compositor,
tabs: &mut self.tabs,
registers: &mut self.registers,
clipboard_history: &mut self.clipboard_history,
local_marks: &mut self.local_marks,
jumplist: &mut self.jumplist,
active_buffer: &mut self.active_buffer,
terminal_size: &mut self.terminal_size,
}
}
}
#[derive(Debug, Clone)]
pub struct ClientSelection {
pub anchor: CursorPosition,
pub cursor: CursorPosition,
pub mode: SelectionMode,
}
impl ClientSelection {
#[must_use]
pub const fn new(anchor: CursorPosition, cursor: CursorPosition, mode: SelectionMode) -> Self {
Self {
anchor,
cursor,
mode,
}
}
#[must_use]
pub fn to_driver_selection(&self) -> Selection {
Selection {
start: self.anchor.into(),
end: self.cursor.into(),
mode: self.mode,
}
}
}
#[cfg(test)]
#[allow(clippy::similar_names)] #[path = "client_tests.rs"]
mod tests;