at main 532 lines 16 kB view raw
1//! Core editor document trait and implementations. 2//! 3//! Defines the `EditorDocument` trait for abstracting editor behavior, 4//! allowing different storage strategies (plain fields vs Signals) while 5//! sharing the core editing logic. 6 7use std::ops::Range; 8 9use smol_str::SmolStr; 10use web_time::Instant; 11 12use crate::text::TextBuffer; 13use crate::types::{BLOCK_SYNTAX_ZONE, CompositionState, CursorState, EditInfo, Selection}; 14use crate::undo::UndoManager; 15 16/// Core trait for editor documents. 17/// 18/// Defines the interface for any editor implementation. Different backends 19/// can implement this trait with different storage strategies: 20/// - `PlainEditor<T>`: Simple field-based storage 21/// - Reactive implementations: Use Signals/state management 22/// 23/// The trait is generic over the buffer type, which must implement both 24/// `TextBuffer` (for text operations) and `UndoManager` (for undo/redo). 25pub trait EditorDocument { 26 /// The buffer type used for text storage and undo. 27 type Buffer: TextBuffer + UndoManager; 28 29 // === Required: Buffer access === 30 31 /// Get a reference to the underlying buffer. 32 fn buffer(&self) -> &Self::Buffer; 33 34 /// Get a mutable reference to the underlying buffer. 35 fn buffer_mut(&mut self) -> &mut Self::Buffer; 36 37 // === Required: Cursor/selection state === 38 39 /// Get the current cursor state. 40 fn cursor(&self) -> CursorState; 41 42 /// Set the cursor state. 43 fn set_cursor(&mut self, cursor: CursorState); 44 45 /// Get the current selection, if any. 46 fn selection(&self) -> Option<Selection>; 47 48 /// Set the selection. 49 fn set_selection(&mut self, selection: Option<Selection>); 50 51 // === Required: Edit tracking === 52 53 /// Get the last edit info, if any. 54 fn last_edit(&self) -> Option<EditInfo>; 55 56 /// Set the last edit info. 57 fn set_last_edit(&mut self, edit: Option<EditInfo>); 58 59 // === Required: Composition (IME) state === 60 61 /// Get the current composition state. 62 fn composition(&self) -> Option<CompositionState>; 63 64 /// Set the composition state. 65 fn set_composition(&mut self, composition: Option<CompositionState>); 66 67 /// Get the timestamp when composition last ended (Safari timing workaround). 68 /// 69 /// Returns None if composition never ended or implementation doesn't track it. 70 fn composition_ended_at(&self) -> Option<web_time::Instant>; 71 72 /// Record that composition ended now (Safari timing workaround). 73 /// 74 /// Implementations that don't need Safari workarounds can make this a no-op. 75 fn set_composition_ended_now(&mut self); 76 77 // === Required: Cursor snap hint === 78 79 /// Get the pending snap direction hint. 80 /// 81 /// This hints which direction the cursor should snap after an edit 82 /// when the cursor lands on invisible syntax. Forward for insertions 83 /// (snap toward new content), backward for deletions (snap toward 84 /// remaining content). 85 fn pending_snap(&self) -> Option<crate::SnapDirection>; 86 87 /// Set the pending snap direction hint. 88 fn set_pending_snap(&mut self, snap: Option<crate::SnapDirection>); 89 90 // === Provided: Convenience accessors === 91 92 /// Get the cursor offset. 93 fn cursor_offset(&self) -> usize { 94 self.cursor().offset 95 } 96 97 /// Set just the cursor offset, preserving other cursor state. 98 fn set_cursor_offset(&mut self, offset: usize) { 99 let mut cursor = self.cursor(); 100 cursor.offset = offset; 101 self.set_cursor(cursor); 102 } 103 104 /// Get the full content as a String. 105 fn content_string(&self) -> String { 106 self.buffer().to_string() 107 } 108 109 /// Get length in characters. 110 fn len_chars(&self) -> usize { 111 self.buffer().len_chars() 112 } 113 114 /// Get length in bytes. 115 fn len_bytes(&self) -> usize { 116 self.buffer().len_bytes() 117 } 118 119 /// Check if document is empty. 120 fn is_empty(&self) -> bool { 121 self.buffer().len_chars() == 0 122 } 123 124 /// Get a slice of the content. 125 fn slice(&self, range: Range<usize>) -> Option<SmolStr> { 126 self.buffer().slice(range) 127 } 128 129 /// Get character at offset. 130 fn char_at(&self, offset: usize) -> Option<char> { 131 self.buffer().char_at(offset) 132 } 133 134 /// Convert char offset to byte offset. 135 fn char_to_byte(&self, char_offset: usize) -> usize { 136 self.buffer().char_to_byte(char_offset) 137 } 138 139 /// Convert byte offset to char offset. 140 fn byte_to_char(&self, byte_offset: usize) -> usize { 141 self.buffer().byte_to_char(byte_offset) 142 } 143 144 /// Get selected text, if any. 145 fn selected_text(&self) -> Option<SmolStr> { 146 self.selection() 147 .and_then(|sel| self.buffer().slice(sel.to_range())) 148 } 149 150 // === Provided: Text operations === 151 152 /// Insert text at char offset, returning edit info. 153 fn insert(&mut self, offset: usize, text: &str) -> EditInfo { 154 let contains_newline = text.contains('\n'); 155 let in_block_syntax_zone = self.is_in_block_syntax_zone(offset); 156 157 self.buffer_mut().insert(offset, text); 158 159 let inserted_len = text.chars().count(); 160 self.set_cursor_offset(offset + inserted_len); 161 162 let edit = EditInfo { 163 edit_char_pos: offset, 164 inserted_len, 165 deleted_len: 0, 166 contains_newline, 167 in_block_syntax_zone, 168 doc_len_after: self.buffer().len_chars(), 169 timestamp: Instant::now(), 170 }; 171 172 self.set_last_edit(Some(edit.clone())); 173 edit 174 } 175 176 /// Delete char range, returning edit info. 177 fn delete(&mut self, range: Range<usize>) -> EditInfo { 178 let deleted_text = self.buffer().slice(range.clone()); 179 let contains_newline = deleted_text 180 .as_ref() 181 .map(|s| s.contains('\n')) 182 .unwrap_or(false); 183 let in_block_syntax_zone = self.is_in_block_syntax_zone(range.start); 184 let deleted_len = range.end - range.start; 185 186 self.buffer_mut().delete(range.clone()); 187 self.set_cursor_offset(range.start); 188 189 let edit = EditInfo { 190 edit_char_pos: range.start, 191 inserted_len: 0, 192 deleted_len, 193 contains_newline, 194 in_block_syntax_zone, 195 doc_len_after: self.buffer().len_chars(), 196 timestamp: Instant::now(), 197 }; 198 199 self.set_last_edit(Some(edit.clone())); 200 edit 201 } 202 203 /// Replace char range with text, returning edit info. 204 fn replace(&mut self, range: Range<usize>, text: &str) -> EditInfo { 205 let deleted_text = self.buffer().slice(range.clone()); 206 let deleted_contains_newline = deleted_text 207 .as_ref() 208 .map(|s| s.contains('\n')) 209 .unwrap_or(false); 210 let contains_newline = text.contains('\n') || deleted_contains_newline; 211 let in_block_syntax_zone = self.is_in_block_syntax_zone(range.start); 212 let deleted_len = range.end - range.start; 213 214 self.buffer_mut().delete(range.clone()); 215 self.buffer_mut().insert(range.start, text); 216 217 let inserted_len = text.chars().count(); 218 self.set_cursor_offset(range.start + inserted_len); 219 220 let edit = EditInfo { 221 edit_char_pos: range.start, 222 inserted_len, 223 deleted_len, 224 contains_newline, 225 in_block_syntax_zone, 226 doc_len_after: self.buffer().len_chars(), 227 timestamp: Instant::now(), 228 }; 229 230 self.set_last_edit(Some(edit.clone())); 231 edit 232 } 233 234 /// Append text at end of document. 235 /// 236 /// This is a fast path for appending - delegates to buffer's push() 237 /// which may have an optimized implementation. 238 fn push(&mut self, text: &str) -> EditInfo { 239 let offset = self.buffer().len_chars(); 240 let contains_newline = text.contains('\n'); 241 let in_block_syntax_zone = self.is_in_block_syntax_zone(offset); 242 243 self.buffer_mut().push(text); 244 245 let inserted_len = text.chars().count(); 246 self.set_cursor_offset(offset + inserted_len); 247 248 let edit = EditInfo { 249 edit_char_pos: offset, 250 inserted_len, 251 deleted_len: 0, 252 contains_newline, 253 in_block_syntax_zone, 254 doc_len_after: self.buffer().len_chars(), 255 timestamp: Instant::now(), 256 }; 257 258 self.set_last_edit(Some(edit.clone())); 259 edit 260 } 261 262 /// Delete the current selection, if any. 263 fn delete_selection(&mut self) -> Option<EditInfo> { 264 let sel = self.selection()?; 265 self.set_selection(None); 266 if sel.is_collapsed() { 267 return None; 268 } 269 Some(self.delete(sel.to_range())) 270 } 271 272 // === Provided: Undo/Redo === 273 274 fn undo(&mut self) -> bool { 275 self.buffer_mut().undo() 276 } 277 278 fn redo(&mut self) -> bool { 279 self.buffer_mut().redo() 280 } 281 282 fn can_undo(&self) -> bool { 283 self.buffer().can_undo() 284 } 285 286 fn can_redo(&self) -> bool { 287 self.buffer().can_redo() 288 } 289 290 fn clear_history(&mut self) { 291 self.buffer_mut().clear_history(); 292 } 293 294 // === Provided: Helpers === 295 296 /// Check if offset is in the block-syntax zone (first ~6 chars of line). 297 fn is_in_block_syntax_zone(&self, offset: usize) -> bool { 298 let mut line_start = offset; 299 while line_start > 0 { 300 if let Some('\n') = self.buffer().char_at(line_start - 1) { 301 break; 302 } 303 line_start -= 1; 304 } 305 offset - line_start < BLOCK_SYNTAX_ZONE 306 } 307} 308 309/// Simple field-based implementation of EditorDocument. 310/// 311/// Stores cursor, selection, and edit state as plain fields. 312/// Use this for non-reactive contexts or as a base for testing. 313#[derive(Clone)] 314pub struct PlainEditor<T: TextBuffer + UndoManager> { 315 buffer: T, 316 cursor: CursorState, 317 selection: Option<Selection>, 318 last_edit: Option<EditInfo>, 319 composition: Option<CompositionState>, 320 composition_ended_at: Option<web_time::Instant>, 321 pending_snap: Option<crate::SnapDirection>, 322} 323 324impl<T: TextBuffer + UndoManager + Default> Default for PlainEditor<T> { 325 fn default() -> Self { 326 Self::new(T::default()) 327 } 328} 329 330impl<T: TextBuffer + UndoManager> PlainEditor<T> { 331 /// Create a new editor with the given buffer. 332 pub fn new(buffer: T) -> Self { 333 Self { 334 buffer, 335 cursor: CursorState::default(), 336 selection: None, 337 last_edit: None, 338 composition: None, 339 composition_ended_at: None, 340 pending_snap: None, 341 } 342 } 343 344 /// Get direct access to the inner buffer (bypasses trait). 345 pub fn inner(&self) -> &T { 346 &self.buffer 347 } 348 349 /// Get direct mutable access to the inner buffer (bypasses trait). 350 pub fn inner_mut(&mut self) -> &mut T { 351 &mut self.buffer 352 } 353} 354 355impl<T: TextBuffer + UndoManager> EditorDocument for PlainEditor<T> { 356 type Buffer = T; 357 358 fn buffer(&self) -> &Self::Buffer { 359 &self.buffer 360 } 361 362 fn buffer_mut(&mut self) -> &mut Self::Buffer { 363 &mut self.buffer 364 } 365 366 fn cursor(&self) -> CursorState { 367 self.cursor.clone() 368 } 369 370 fn set_cursor(&mut self, cursor: CursorState) { 371 self.cursor = cursor; 372 } 373 374 fn selection(&self) -> Option<Selection> { 375 self.selection.clone() 376 } 377 378 fn set_selection(&mut self, selection: Option<Selection>) { 379 self.selection = selection; 380 } 381 382 fn last_edit(&self) -> Option<EditInfo> { 383 self.last_edit.clone() 384 } 385 386 fn set_last_edit(&mut self, edit: Option<EditInfo>) { 387 self.last_edit = edit; 388 } 389 390 fn composition(&self) -> Option<CompositionState> { 391 self.composition.clone() 392 } 393 394 fn set_composition(&mut self, composition: Option<CompositionState>) { 395 self.composition = composition; 396 } 397 398 fn composition_ended_at(&self) -> Option<web_time::Instant> { 399 self.composition_ended_at 400 } 401 402 fn set_composition_ended_now(&mut self) { 403 self.composition_ended_at = Some(web_time::Instant::now()); 404 } 405 406 fn pending_snap(&self) -> Option<crate::SnapDirection> { 407 self.pending_snap 408 } 409 410 fn set_pending_snap(&mut self, snap: Option<crate::SnapDirection>) { 411 self.pending_snap = snap; 412 } 413} 414 415#[cfg(test)] 416mod tests { 417 use super::*; 418 use crate::{EditorRope, UndoableBuffer}; 419 420 type TestEditor = PlainEditor<UndoableBuffer<EditorRope>>; 421 422 fn make_editor(content: &str) -> TestEditor { 423 let rope = EditorRope::from_str(content); 424 let buf = UndoableBuffer::new(rope, 100); 425 PlainEditor::new(buf) 426 } 427 428 #[test] 429 fn test_basic_insert() { 430 let mut editor = make_editor("hello"); 431 assert_eq!(editor.content_string(), "hello"); 432 433 let edit = editor.insert(5, " world"); 434 assert_eq!(editor.content_string(), "hello world"); 435 assert_eq!(edit.inserted_len, 6); 436 assert_eq!(editor.cursor_offset(), 11); 437 } 438 439 #[test] 440 fn test_delete() { 441 let mut editor = make_editor("hello world"); 442 443 let edit = editor.delete(5..11); 444 assert_eq!(editor.content_string(), "hello"); 445 assert_eq!(edit.deleted_len, 6); 446 assert_eq!(editor.cursor_offset(), 5); 447 } 448 449 #[test] 450 fn test_replace() { 451 let mut editor = make_editor("hello world"); 452 453 let edit = editor.replace(6..11, "rust"); 454 assert_eq!(editor.content_string(), "hello rust"); 455 assert_eq!(edit.deleted_len, 5); 456 assert_eq!(edit.inserted_len, 4); 457 } 458 459 #[test] 460 fn test_undo_redo() { 461 let mut editor = make_editor("hello"); 462 463 editor.insert(5, " world"); 464 assert_eq!(editor.content_string(), "hello world"); 465 466 assert!(editor.undo()); 467 assert_eq!(editor.content_string(), "hello"); 468 469 assert!(editor.redo()); 470 assert_eq!(editor.content_string(), "hello world"); 471 } 472 473 #[test] 474 fn test_selection() { 475 let mut editor = make_editor("hello world"); 476 477 editor.set_selection(Some(Selection::new(0, 5))); 478 assert_eq!(editor.selected_text(), Some("hello".into())); 479 480 let edit = editor.delete_selection(); 481 assert!(edit.is_some()); 482 assert_eq!(editor.content_string(), " world"); 483 assert!(editor.selection().is_none()); 484 } 485 486 #[test] 487 fn test_block_syntax_zone() { 488 let mut editor = make_editor("# heading\nparagraph"); 489 490 // Position 0 is in block syntax zone 491 let edit = editor.insert(0, "x"); 492 assert!(edit.in_block_syntax_zone); 493 494 // Position after newline (start of "paragraph") is also in zone 495 // Original was "# heading\nparagraph", after insert "x# heading\nparagraph" 496 // Position 11 is start of "paragraph" line 497 let edit = editor.insert(11, "y"); 498 assert!(edit.in_block_syntax_zone); 499 } 500 501 #[test] 502 fn test_composition_state() { 503 let mut editor = make_editor("hello"); 504 505 assert!(editor.composition().is_none()); 506 507 let comp = CompositionState::new(5, "".into()); 508 editor.set_composition(Some(comp.clone())); 509 510 assert_eq!(editor.composition(), Some(comp)); 511 512 editor.set_composition(None); 513 assert!(editor.composition().is_none()); 514 } 515 516 #[test] 517 fn test_offset_conversions() { 518 let editor = make_editor("héllo wörld"); // multi-byte chars 519 520 // 'é' is 2 bytes, 'ö' is 2 bytes 521 // chars: h é l l o w ö r l d 522 // idx: 0 1 2 3 4 5 6 7 8 9 10 523 524 assert_eq!(editor.len_chars(), 11); 525 assert!(editor.len_bytes() > 11); // multi-byte chars 526 527 // char 1 ('é') starts at byte 1 528 assert_eq!(editor.char_to_byte(1), 1); 529 // char 2 ('l') starts after 'é' (2 bytes) 530 assert_eq!(editor.char_to_byte(2), 3); 531 } 532}