Skip to main content

cdx_core/extensions/collaboration/
session.rs

1//! Collaboration session management for Codex documents.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::DocumentId;
7
8use super::Collaborator;
9
10/// A collaborative editing session.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct CollaborationSession {
14    /// Session ID.
15    pub id: String,
16
17    /// Document being collaborated on.
18    pub document_id: DocumentId,
19
20    /// Current participants.
21    pub participants: Vec<Participant>,
22
23    /// When the session started.
24    pub started: DateTime<Utc>,
25
26    /// Session status.
27    pub status: SessionStatus,
28}
29
30impl CollaborationSession {
31    /// Create a new collaboration session.
32    #[must_use]
33    pub fn new(id: impl Into<String>, document_id: DocumentId) -> Self {
34        Self {
35            id: id.into(),
36            document_id,
37            participants: Vec::new(),
38            started: Utc::now(),
39            status: SessionStatus::Active,
40        }
41    }
42
43    /// Add a participant.
44    pub fn add_participant(&mut self, participant: Participant) {
45        self.participants.push(participant);
46    }
47
48    /// Remove a participant by user ID.
49    pub fn remove_participant(&mut self, user_id: &str) {
50        self.participants
51            .retain(|p| p.author.user_id.as_deref() != Some(user_id));
52    }
53
54    /// Get the number of participants.
55    #[must_use]
56    pub fn participant_count(&self) -> usize {
57        self.participants.len()
58    }
59
60    /// End the session.
61    pub fn end(&mut self) {
62        self.status = SessionStatus::Ended;
63    }
64}
65
66/// A participant in a collaboration session.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Participant {
70    /// Author information.
71    pub author: Collaborator,
72
73    /// When the participant joined.
74    pub joined: DateTime<Utc>,
75
76    /// Participant's cursor position.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub cursor: Option<CursorPosition>,
79
80    /// Assigned color for this participant.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub color: Option<String>,
83
84    /// Current selection.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub selection: Option<Selection>,
87}
88
89impl Participant {
90    /// Create a new participant.
91    #[must_use]
92    pub fn new(author: Collaborator) -> Self {
93        Self {
94            author,
95            joined: Utc::now(),
96            cursor: None,
97            color: None,
98            selection: None,
99        }
100    }
101
102    /// Set cursor position.
103    #[must_use]
104    pub fn with_cursor(mut self, cursor: CursorPosition) -> Self {
105        self.cursor = Some(cursor);
106        self
107    }
108
109    /// Set assigned color.
110    #[must_use]
111    pub fn with_color(mut self, color: impl Into<String>) -> Self {
112        self.color = Some(color.into());
113        self
114    }
115}
116
117/// Cursor position in the document.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct CursorPosition {
121    /// Block containing the cursor.
122    pub block_ref: String,
123
124    /// Offset within the block.
125    pub offset: usize,
126}
127
128impl CursorPosition {
129    /// Create a new cursor position.
130    #[must_use]
131    pub fn new(block_ref: impl Into<String>, offset: usize) -> Self {
132        Self {
133            block_ref: block_ref.into(),
134            offset,
135        }
136    }
137}
138
139/// A text selection in the document.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct Selection {
143    /// Start position.
144    pub start: CursorPosition,
145
146    /// End position.
147    pub end: CursorPosition,
148}
149
150impl Selection {
151    /// Create a new selection.
152    #[must_use]
153    pub fn new(start: CursorPosition, end: CursorPosition) -> Self {
154        Self { start, end }
155    }
156
157    /// Create a selection within a single block.
158    #[must_use]
159    pub fn within_block(
160        block_ref: impl Into<String>,
161        start_offset: usize,
162        end_offset: usize,
163    ) -> Self {
164        let block_ref = block_ref.into();
165        Self {
166            start: CursorPosition::new(block_ref.clone(), start_offset),
167            end: CursorPosition::new(block_ref, end_offset),
168        }
169    }
170}
171
172/// Status of a collaboration session.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum SessionStatus {
176    /// Session is active.
177    #[default]
178    Active,
179    /// Session is paused.
180    Paused,
181    /// Session has ended.
182    Ended,
183}