Skip to main content

oo_ide/editor/
buffer.rs

1//! Core text buffer backed by ropey.
2//!
3//! This buffer provides:
4//! - Efficient text storage via ropey
5//! - Position-based cursor and selection
6//! - Basic undo/redo via snapshots
7//! - Async-safe snapshots
8
9use std::{
10    fs,
11    ops::Range,
12    path::{Path, PathBuf},
13};
14
15use std::sync::Arc;
16
17use ropey::{LineType, Rope};
18use serde::{Deserialize, Serialize};
19
20use crate::editor::history::{apply_changeset, BufferOp, ChangeKind, ChangeSet, History};
21use crate::editor::position::Position;
22use crate::editor::selection::Selection;
23use crate::prelude::*;
24use crate::settings::adapters;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct BufferId(u64);
28
29impl BufferId {
30    pub fn new() -> Self {
31        use std::sync::atomic::{AtomicU64, Ordering};
32        static COUNTER: AtomicU64 = AtomicU64::new(1);
33        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
34    }
35}
36
37impl Default for BufferId {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
44pub struct Version(u64);
45
46impl Version {
47    pub fn new() -> Self {
48        Self(0)
49    }
50
51    pub fn value(&self) -> u64 {
52        self.0
53    }
54
55    /// Create a version with a specific raw counter value. Used in tests.
56    #[cfg(test)]
57    pub(crate) fn from_raw(v: u64) -> Self {
58        Self(v)
59    }
60
61    fn increment(&mut self) {
62        self.0 += 1;
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SerializableSnapshot {
68    lines: Vec<String>,
69    cursor: Position,
70    #[serde(default)]
71    selection: Option<Selection>,
72    dirty: bool,
73    version: Version,
74}
75
76impl From<&Buffer> for SerializableSnapshot {
77    fn from(buf: &Buffer) -> Self {
78        let lines = buf.lines();
79        SerializableSnapshot {
80            lines,
81            cursor: buf.cursor,
82            selection: buf.selection,
83            dirty: buf.dirty,
84            version: buf.version,
85        }
86    }
87}
88
89impl SerializableSnapshot {
90    pub fn lines(&self) -> &[String] {
91        &self.lines
92    }
93
94    pub fn cursor(&self) -> Position {
95        self.cursor
96    }
97
98    pub fn selection(&self) -> Option<Selection> {
99        self.selection
100    }
101
102    pub fn dirty(&self) -> bool {
103        self.dirty
104    }
105
106    pub fn version(&self) -> Version {
107        self.version
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct BufferSnapshot {
113    pub text: Rope,
114    pub version: Version,
115}
116
117impl BufferSnapshot {
118    pub fn lines(&self) -> Vec<String> {
119        let line_type = LineType::LF;
120        (0..self.text.len_lines(line_type))
121            .map(|i| {
122                let mut line_text = self.text.line(i, line_type).to_string();
123                if line_text.ends_with('\n') {
124                    line_text.pop();
125                }
126                line_text
127            })
128            .collect()
129    }
130
131    pub fn line_count(&self) -> usize {
132        self.text.len_lines(LineType::LF)
133    }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct Marker {
138    pub line: usize,
139    pub label: String,
140}
141
142/// Accumulates `BufferOp`s during an open transaction so they can be
143/// committed to history as a single undo step via [`Buffer::end_transaction`].
144#[derive(Debug)]
145struct Transaction {
146    ops: Vec<BufferOp>,
147    kind: ChangeKind,
148    before_cursor: Position,
149    before_selection: Option<Selection>,
150}
151
152#[derive(Debug)]
153pub struct Buffer {
154    id: BufferId,
155    version: Version,
156    text: Rope,
157    pub path: Option<PathBuf>,
158    cursor: Position,
159    selection: Option<Selection>,
160    pub scroll: usize,
161    pub scroll_x: usize,
162    dirty: bool,
163    history: History,
164    pub markers: Vec<Marker>,
165    /// When `Some`, all calls to [`apply_change`] buffer their ops here
166    /// instead of touching history directly.
167    active_tx: Option<Transaction>,
168    /// Hash of the file content at last save (for detecting external modifications).
169    pub file_hash: Option<u64>,
170    /// Modification time of the file at last save (for quick pre-check).
171    pub file_mtime: Option<u64>,
172    /// Size of the file at last save (for quick pre-check).
173    pub file_size: Option<u64>,
174    /// Set to true when an external modification is detected.
175    pub external_modification_detected: bool,
176    /// Set to true during save to ignore file watcher events.
177    pub is_saving: bool,
178    /// Cached shared lines for cheap cloning across highlight requests.
179    cached_lines: Option<Arc<Vec<String>>>,
180}
181
182impl Buffer {
183    pub fn new() -> Self {
184        Self {
185            id: BufferId::new(),
186            version: Version::new(),
187            text: Rope::new(),
188            path: None,
189            cursor: Position::default(),
190            selection: None,
191            scroll: 0,
192            scroll_x: 0,
193            dirty: false,
194            history: History::new(),
195            markers: Vec::new(),
196            active_tx: None,
197            file_hash: None,
198            file_mtime: None,
199            file_size: None,
200            external_modification_detected: false,
201            is_saving: false,
202            cached_lines: None,
203        }
204    }
205
206    pub fn from_text(text: &str) -> Self {
207        let mut buf = Self::new();
208        buf.text = Rope::from_str(text);
209        let line_count = buf.text.len_lines(LineType::LF);
210        if line_count > 0 {
211            let last_line_idx = line_count - 1;
212            let last_line_len = buf.text.line(last_line_idx, LineType::LF).len_chars();
213            buf.cursor = Position::new(last_line_idx, last_line_len);
214        }
215        buf
216    }
217
218    /// Test-only constructor that disables history merging so every edit
219    /// becomes its own undo step, regardless of how quickly it happens.
220    #[cfg(test)]
221    pub(crate) fn from_text_no_merge(text: &str) -> Self {
222        let mut buf = Self::from_text(text);
223        buf.history = History::with_threshold(0);
224        buf
225    }
226
227    pub fn from_lines(lines: Vec<String>, path: Option<PathBuf>) -> Self {
228        let text = if lines.is_empty() {
229            String::new()
230        } else {
231            lines.join("\n")
232        };
233        let mut buf = Self::new();
234        buf.text = Rope::from_str(&text);
235        buf.path = path;
236        buf.cursor = Position::new(0, 0);
237        buf
238    }
239
240    pub fn open(path: &Path) -> Result<Self> {
241        let content = fs::read_to_string(path)?;
242        let mut buf = Self::new();
243        buf.text = Rope::from_str(&content);
244        buf.path = Some(path.to_path_buf());
245        buf.normalize_cursor();
246        Ok(buf)
247    }
248
249    #[allow(clippy::too_many_arguments)]
250    pub fn restore(
251        path: PathBuf,
252        lines: Vec<String>,
253        selection: Option<Selection>,
254        scroll: usize,
255        dirty: bool,
256        markers: Vec<Marker>,
257        undo_stack: Vec<SerializableSnapshot>,
258        redo_stack: Vec<SerializableSnapshot>,
259    ) -> Self {
260        let text = if lines.is_empty() {
261            Rope::new()
262        } else {
263            Rope::from_str(&lines.join("\n"))
264        };
265        let cursor = selection.as_ref().map(|s| s.active).unwrap_or_default();
266        let version = Version::new();
267        let mut history = History::new();
268        history.restore_from_snapshots(undo_stack, redo_stack, version);
269        let file_hash = Self::compute_hash(&text);
270        let file_metadata = Self::get_file_metadata(&path);
271        let file_mtime = file_metadata.as_ref().map(|(m, _)| *m);
272        let file_size = file_metadata.map(|(_, s)| s);
273        Self {
274            id: BufferId::new(),
275            version,
276            text,
277            path: Some(path),
278            cursor,
279            selection,
280            scroll,
281            scroll_x: 0,
282            dirty,
283            history,
284            markers,
285            active_tx: None,
286            file_hash,
287            file_mtime,
288            file_size,
289            external_modification_detected: false,
290            is_saving: false,
291            cached_lines: None,
292        }
293    }
294
295    pub fn id(&self) -> BufferId {
296        self.id
297    }
298
299    pub fn version(&self) -> Version {
300        self.version
301    }
302
303    pub fn snapshot(&self) -> BufferSnapshot {
304        BufferSnapshot {
305            text: self.text.clone(),
306            version: self.version,
307        }
308    }
309
310    fn line_type(&self) -> LineType {
311        LineType::LF
312    }
313
314    fn pos_to_byte(&self, pos: Position) -> usize {
315        let line_count = self.text.len_lines(self.line_type());
316        if pos.line >= line_count {
317            if pos.line == line_count && pos.column == 0 && line_count > 0 {
318                let last_line_idx = line_count - 1;
319                let last_line = self.text.line(last_line_idx, self.line_type());
320                if last_line.chars().last() == Some('\n') {
321                    let last_line_start =
322                        self.text.line_to_byte_idx(last_line_idx, self.line_type());
323                    return last_line_start + last_line.len() - 1;
324                }
325            }
326            return self.text.len();
327        }
328        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
329        let line = self.text.line(pos.line, self.line_type());
330
331        let mut char_count = 0;
332        let mut byte_offset = 0;
333        for ch in line.chars() {
334            if char_count >= pos.column {
335                break;
336            }
337            byte_offset += ch.len_utf8();
338            char_count += 1;
339        }
340        if pos.column > char_count {
341            byte_offset = line.len();
342        }
343        line_start_byte + byte_offset
344    }
345
346    fn byte_to_pos(&self, byte_idx: usize) -> Position {
347        let clamped_byte_idx = byte_idx.min(self.text.len());
348        let line_count = self.text.len_lines(self.line_type());
349
350        if clamped_byte_idx == self.text.len() {
351            if line_count == 0 {
352                return Position::new(0, 0);
353            }
354            let last_line_idx = line_count - 1;
355            let last_line = self.text.line(last_line_idx, self.line_type());
356            let last_line_len = last_line.len_chars();
357            let has_trailing_nl = last_line.chars().last() == Some('\n');
358            if has_trailing_nl {
359                return Position::new(last_line_idx + 1, 0);
360            }
361            return Position::new(last_line_idx, last_line_len);
362        }
363
364        let byte_idx = clamped_byte_idx;
365        let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
366        let line_start_byte = self.text.line_to_byte_idx(line_idx, self.line_type());
367        let char_idx = self.text.byte_to_char_idx(byte_idx);
368        let line_start_char = self.text.byte_to_char_idx(line_start_byte);
369        let line = self.text.line(line_idx, self.line_type());
370        let line_len_chars = line.len_chars();
371        let line_has_trailing_nl = line.chars().last() == Some('\n');
372
373        let column = char_idx - line_start_char;
374        if column >= line_len_chars && line_has_trailing_nl {
375            return Position::new(line_idx + 1, 0);
376        }
377        Position::new(line_idx, column)
378    }
379
380    fn cursor_to_byte(&self) -> usize {
381        self.pos_to_byte(self.cursor)
382    }
383
384    /// Convert a `Position` to a global char index using ropey's char metric.
385    pub fn pos_to_char(&self, pos: Position) -> usize {
386        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
387        let line_start_char = self.text.byte_to_char_idx(line_start_byte);
388        line_start_char + pos.column
389    }
390
391    /// Convert a global char index to a `Position`.
392    pub fn char_to_pos(&self, char_idx: usize) -> Position {
393        let byte_idx = self.text.char_to_byte_idx(char_idx).min(self.text.len());
394        self.byte_to_pos(byte_idx)
395    }
396
397    pub fn cursor(&self) -> Position {
398        self.cursor
399    }
400
401    pub fn selection(&self) -> Option<Selection> {
402        self.selection
403    }
404
405    pub fn set_cursor(&mut self, pos: Position) {
406        self.selection = None;
407        self.cursor = self.normalize_position(pos);
408    }
409
410    pub fn set_selection(&mut self, selection: Option<Selection>) {
411        self.selection = selection.map(|sel| Selection {
412            anchor: self.normalize_position(sel.anchor),
413            active: self.normalize_position(sel.active),
414        });
415        if let Some(ref sel) = self.selection {
416            self.cursor = sel.active;
417        }
418    }
419
420    /// Clamp `pos` to a valid (line, column) within the current text.
421    pub fn normalize_position(&self, pos: Position) -> Position {
422        let line_count = self.text.len_lines(self.line_type());
423        if line_count == 0 {
424            return Position::new(0, 0);
425        }
426        let line = pos.line.min(line_count - 1);
427        let line_len = self.line_display_len(line);
428        Position::new(line, pos.column.min(line_len))
429    }
430
431    pub fn normalize_cursor(&mut self) {
432        self.cursor = self.normalize_position(self.cursor);
433    }
434
435    /// Return the usable character length of a line, excluding the trailing newline.
436    fn line_display_len(&self, line_idx: usize) -> usize {
437        let line = self.text.line(line_idx, self.line_type());
438        let has_trailing_nl = line.chars().last() == Some('\n');
439        line.len_chars().saturating_sub(has_trailing_nl as usize)
440    }
441
442    pub fn line_count(&self) -> usize {
443        self.text.len_lines(self.line_type()).max(1)
444    }
445
446    pub fn current_line(&self) -> usize {
447        self.cursor.line
448    }
449
450    pub fn char_count(&self) -> usize {
451        self.text.len_chars()
452    }
453
454    pub fn line(&self, line_idx: usize) -> Option<String> {
455        let actual_line_count = self.text.len_lines(self.line_type());
456        if line_idx >= actual_line_count {
457            return None;
458        }
459        let mut line_text = self.text.line(line_idx, self.line_type()).to_string();
460        if line_text.ends_with('\n') {
461            line_text.pop();
462        }
463        if line_text.ends_with('\r') {
464            line_text.pop();
465        }
466        Some(line_text)
467    }
468
469    pub fn lines(&self) -> Vec<String> {
470        (0..self.text.len_lines(self.line_type()))
471            .map(|i| {
472                let mut s: String = self.text.line(i, self.line_type()).into();
473                if s.ends_with('\n') {
474                    s.pop();
475                }
476                if s.ends_with('\r') {
477                    s.pop();
478                }
479                s
480            })
481            .collect()
482    }
483
484    /// Return a shared `Arc<Vec<String>>` for the current buffer contents.
485    /// The result is cached and invalidated on mutations to avoid rebuilding
486    /// the Vec for every highlight request.
487    pub fn lines_arc(&mut self) -> Arc<Vec<String>> {
488        if let Some(ref cached) = self.cached_lines {
489            return Arc::clone(cached);
490        }
491        let v = self.lines();
492        let arc = Arc::new(v);
493        self.cached_lines = Some(arc.clone());
494        arc
495    }
496
497    pub fn is_empty(&self) -> bool {
498        self.text.len_chars() == 0
499    }
500
501    pub fn is_dirty(&self) -> bool {
502        self.dirty
503    }
504
505    fn increment_version(&mut self) {
506        self.version.increment();
507        self.dirty = true;
508        self.file_hash = None;
509        self.file_mtime = None;
510        self.file_size = None;
511        self.cached_lines = None;
512    }
513
514    pub fn can_undo(&self) -> bool {
515        self.history.can_undo()
516    }
517
518    pub fn can_redo(&self) -> bool {
519        self.history.can_redo()
520    }
521
522    pub fn history_len(&self) -> usize {
523        self.history.len()
524    }
525
526    pub fn history_position(&self) -> usize {
527        self.history.current_position()
528    }
529
530    pub fn apply_change(&mut self, change: ChangeSet, kind: ChangeKind) {
531        if change.is_empty() {
532            return;
533        }
534
535        // Inside a transaction: buffer the raw ops and return; history is
536        // not touched until end_transaction() commits everything at once.
537        if let Some(tx) = &mut self.active_tx {
538            tx.ops.extend(change.ops);
539            return;
540        }
541
542        // Coalesce adjacent typing/deletion into a single undo step when
543        // the edits are contiguous, within the merge window, and don't
544        // cross line boundaries.  All other kinds (Replace, Structural, …)
545        // always get their own entry so they can be undone atomically.
546        let merged = matches!(kind, ChangeKind::InsertText | ChangeKind::DeleteText)
547            && self.history.try_merge(change.clone(), kind, self.version);
548
549        if !merged {
550            self.history.push(change, kind, self.version);
551        }
552    }
553
554    /// Begin an explicit transaction.  All subsequent calls to
555    /// [`Buffer::apply_change`] will buffer their ops instead of writing to history.
556    /// Call [`Buffer::end_transaction`] to commit everything as one undo step.
557    ///
558    /// If a transaction is already active this is a no-op (the outermost
559    /// transaction wins).  In debug builds a panic is emitted to surface
560    /// accidental nesting early.
561    pub fn begin_transaction(&mut self, kind: ChangeKind) {
562        debug_assert!(
563            self.active_tx.is_none(),
564            "begin_transaction called while a transaction is already active; \
565             nested transactions are not supported"
566        );
567        if self.active_tx.is_some() {
568            return;
569        }
570        self.active_tx = Some(Transaction {
571            ops: Vec::new(),
572            kind,
573            before_cursor: self.cursor,
574            before_selection: self.selection,
575        });
576    }
577
578    /// Commit the current transaction to history as a **single undo step**.
579    ///
580    /// * The committed entry always gets its own history slot — `try_merge`
581    ///   is bypassed, so the transaction is never accidentally coalesced with
582    ///   surrounding typing.
583    /// * `history.push` truncates any future (redo) entries, so a
584    ///   transaction after undo correctly discards the forward branch.
585    /// * `increment_version` is called on commit so the buffer version
586    ///   advances once for the whole group (consistent with LSP sync).
587    ///
588    /// If no transaction is active, or if no ops were recorded, this is a
589    /// no-op.
590    pub fn end_transaction(&mut self) {
591        let tx = match self.active_tx.take() {
592            Some(tx) => tx,
593            None => return,
594        };
595        if tx.ops.is_empty() {
596            return;
597        }
598        let change = ChangeSet::new(
599            tx.ops,
600            tx.before_cursor,
601            self.cursor,
602            tx.before_selection,
603            self.selection,
604        );
605        // Always push; the transaction *is* the merge unit.
606        // push() also truncates the redo stack, so branching history is safe.
607        self.history.push(change, tx.kind, self.version);
608        // Advance the version once for the committed group so that external
609        // consumers (LSP, syntax highlighter) see exactly one change event.
610        self.increment_version();
611    }
612
613    /// Discard the current transaction **without** recording a history entry.
614    ///
615    /// ⚠️  Text mutations that occurred within the transaction are **not**
616    /// rolled back — only the pending history entry is dropped.  The buffer
617    /// will be in a modified-but-untracked state after this call.  Use this
618    /// only when you intend to handle the rollback yourself (e.g. by
619    /// reloading from disk), or when you are certain no edits were made.
620    pub fn abort_transaction(&mut self) {
621        self.active_tx = None;
622    }
623
624    /// Returns `true` if a transaction is currently open.
625    pub fn in_transaction(&self) -> bool {
626        self.active_tx.is_some()
627    }
628
629    pub fn insert(&mut self, text: &str) {
630        // If there's a selection, replace it atomically as a single undo step.
631        if let Some(sel) = self.selection {
632            let start = sel.min();
633            let end = sel.max();
634            let before_cursor = self.cursor;
635            let before_selection = self.selection;
636
637            let start_byte = self.pos_to_byte(start);
638            let end_byte = self.pos_to_byte(end).min(self.text.len());
639            let old_text: String = self.text.slice(start_byte..end_byte).into();
640
641            self.text.remove(start_byte..end_byte);
642            self.text.insert(start_byte, text);
643
644            let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
645            let after_cursor = self.byte_to_pos(
646                self.text
647                    .char_to_byte_idx(new_char_idx)
648                    .min(self.text.len()),
649            );
650            self.cursor = after_cursor;
651            self.selection = None;
652
653            let op = BufferOp::Replace {
654                range: start..end,
655                text: text.to_string(),
656                old_text,
657                end_position: after_cursor,
658            };
659            let change = ChangeSet::new(
660                vec![op],
661                before_cursor,
662                after_cursor,
663                before_selection,
664                None,
665            );
666            self.apply_change(change, ChangeKind::Replace);
667            self.increment_version();
668            return;
669        }
670
671        let before_cursor = self.cursor;
672        let before_selection = self.selection;
673
674        let byte_idx = self.cursor_to_byte();
675        let old_char_idx = self.text.byte_to_char_idx(byte_idx);
676        self.text.insert(byte_idx, text);
677
678        let new_char_idx = old_char_idx + text.chars().count();
679        let new_char_byte = self
680            .text
681            .char_to_byte_idx(new_char_idx)
682            .min(self.text.len());
683        let new_cursor = self.byte_to_pos(new_char_byte);
684        self.cursor = new_cursor;
685
686        let after_cursor = new_cursor;
687        let after_selection = None;
688
689        let range = before_cursor..after_cursor;
690        let op = BufferOp::Replace {
691            range,
692            text: text.to_string(),
693            old_text: String::new(),
694            end_position: after_cursor,
695        };
696
697        let change = ChangeSet::new(
698            vec![op],
699            before_cursor,
700            after_cursor,
701            before_selection,
702            after_selection,
703        );
704
705        self.apply_change(change, ChangeKind::InsertText);
706        self.increment_version();
707    }
708
709    /// Delete the current selection if one exists. Returns true if a selection was deleted.
710    fn delete_selection_if_active(&mut self) -> bool {
711        if let Some(sel) = self.selection {
712            self.delete_range(sel.min(), sel.max());
713            true
714        } else {
715            false
716        }
717    }
718
719    pub fn delete_backward(&mut self) {
720        if self.delete_selection_if_active() {
721            return;
722        }
723
724        let byte_idx = self.cursor_to_byte();
725        if byte_idx == 0 {
726            return;
727        }
728
729        let before_cursor = self.cursor;
730        let before_selection = self.selection;
731
732        let char_idx = self.text.byte_to_char_idx(byte_idx);
733        if char_idx == 0 {
734            return;
735        }
736        let prev_char_idx = char_idx - 1;
737        let del_start_byte = self.text.char_to_byte_idx(prev_char_idx);
738        let del_end_byte = byte_idx;
739        let prev_char_byte_idx = del_start_byte;
740        let prev_char = self.text.char(prev_char_byte_idx);
741
742        let new_cursor_pos: Position;
743        if prev_char == '\n' {
744            let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
745            if line_idx > 0 {
746                let new_cursor_col = self.line_display_len(line_idx - 1);
747                new_cursor_pos = Position::new(line_idx - 1, new_cursor_col);
748            } else {
749                new_cursor_pos = before_cursor;
750            }
751        } else {
752            let new_col = before_cursor.column.saturating_sub(1);
753            new_cursor_pos = Position::new(before_cursor.line, new_col);
754        }
755
756        let deleted_text: String = self.text.slice(del_start_byte..del_end_byte).into();
757
758        self.text.remove(del_start_byte..del_end_byte);
759
760        self.cursor = new_cursor_pos;
761        self.selection = None;
762
763        let range = new_cursor_pos..before_cursor;
764        let op = BufferOp::Replace {
765            range,
766            text: String::new(),
767            old_text: deleted_text,
768            end_position: new_cursor_pos,
769        };
770
771        let change = ChangeSet::new(
772            vec![op],
773            before_cursor,
774            new_cursor_pos,
775            before_selection,
776            None,
777        );
778
779        self.apply_change(change, ChangeKind::DeleteText);
780        self.increment_version();
781    }
782
783    pub fn delete_forward(&mut self) {
784        if self.delete_selection_if_active() {
785            return;
786        }
787
788        let byte_idx = self.cursor_to_byte();
789        if byte_idx >= self.text.len() {
790            return;
791        }
792
793        let before_cursor = self.cursor;
794        let before_selection = self.selection;
795
796        // Advance by exactly one char using char indices (correct for multi-byte UTF-8).
797        let char_idx = self.text.byte_to_char_idx(byte_idx);
798        let next_byte = self
799            .text
800            .char_to_byte_idx(char_idx + 1)
801            .min(self.text.len());
802
803        let deleted_text: String = self.text.slice(byte_idx..next_byte).into();
804        self.text.remove(byte_idx..next_byte);
805
806        let after_cursor = before_cursor;
807        let after_selection = None;
808
809        let range = before_cursor..after_cursor;
810        let op = BufferOp::Replace {
811            range,
812            text: String::new(),
813            old_text: deleted_text,
814            end_position: before_cursor,
815        };
816
817        let change = ChangeSet::new(
818            vec![op],
819            before_cursor,
820            after_cursor,
821            before_selection,
822            after_selection,
823        );
824
825        self.apply_change(change, ChangeKind::DeleteText);
826        self.increment_version();
827    }
828
829    pub fn delete_range(&mut self, start_pos: Position, end_pos: Position) {
830        if start_pos == end_pos {
831            return;
832        }
833
834        let start_byte = self.pos_to_byte(start_pos);
835        let end_byte = self.pos_to_byte(end_pos).min(self.text.len());
836
837        if start_byte >= self.text.len() || start_byte >= end_byte {
838            return;
839        }
840
841        let before_cursor = self.cursor;
842        let before_selection = self.selection;
843
844        let deleted_text: String = self.text.slice(start_byte..end_byte).into();
845        self.text.remove(start_byte..end_byte);
846
847        self.cursor = start_pos;
848        self.selection = None;
849
850        let range = start_pos..end_pos;
851        let op = BufferOp::Replace {
852            range,
853            text: String::new(),
854            old_text: deleted_text,
855            end_position: start_pos,
856        };
857
858        let change = ChangeSet::new(vec![op], before_cursor, start_pos, before_selection, None);
859
860        self.apply_change(change, ChangeKind::DeleteText);
861        self.increment_version();
862    }
863
864    pub fn replace_range(&mut self, start: Position, end: Position, text: &str) {
865        let start_byte = self.pos_to_byte(start);
866        let end_byte = self.pos_to_byte(end).min(self.text.len());
867
868        if start_byte >= self.text.len() && start != end {
869            return;
870        }
871
872        let before_cursor = self.cursor;
873        let before_selection = self.selection;
874
875        let actual_end_byte = end_byte.min(self.text.len());
876        let old_text: String = self.text.slice(start_byte..actual_end_byte).into();
877
878        if old_text == text && before_cursor == start {
879            return;
880        }
881
882        self.text.remove(start_byte..actual_end_byte);
883        self.text.insert(start_byte, text);
884
885        let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
886        let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
887        self.cursor = after_cursor;
888        self.selection = None;
889
890        let range = start..end;
891        let op = BufferOp::Replace {
892            range,
893            text: text.to_string(),
894            old_text,
895            end_position: after_cursor,
896        };
897
898        let change = ChangeSet::new(
899            vec![op],
900            before_cursor,
901            after_cursor,
902            before_selection,
903            None,
904        );
905
906        self.apply_change(change, ChangeKind::Replace);
907        self.increment_version();
908    }
909
910    pub fn insert_newline(&mut self) {
911        let before_cursor = self.cursor;
912        let before_selection = self.selection;
913
914        let byte_idx = self.cursor_to_byte();
915        self.text.insert(byte_idx, "\n");
916
917        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + 1;
918        let new_cursor_byte = self
919            .text
920            .char_to_byte_idx(new_char_idx)
921            .min(self.text.len());
922        let new_cursor = self.byte_to_pos(new_cursor_byte);
923        self.cursor = new_cursor;
924
925        let range = before_cursor..new_cursor;
926        let op = BufferOp::Replace {
927            range,
928            text: "\n".to_string(),
929            old_text: String::new(),
930            end_position: new_cursor,
931        };
932
933        let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
934
935        self.apply_change(change, ChangeKind::Structural);
936        self.increment_version();
937    }
938
939    pub fn insert_newline_with_indent(&mut self, _use_spaces: bool, _indent_width: usize) {
940        // Extract leading whitespace from the current line
941        // This ensures we carry forward the same indentation for the new line
942        let leading_indent = if self.cursor.line < self.text.len_lines(self.line_type()) {
943            let line = self.text.line(self.cursor.line, self.line_type());
944            line.chars()
945                .take_while(|c| *c == ' ' || *c == '\t')
946                .collect::<String>()
947        } else {
948            String::new()
949        };
950
951        // Always use the leading indentation from the current line (even if empty)
952        // This matches the user's existing indentation pattern
953        let text = format!("\n{}", leading_indent);
954
955        let before_cursor = self.cursor;
956        let before_selection = self.selection;
957
958        let byte_idx = self.cursor_to_byte();
959        self.text.insert(byte_idx, &text);
960
961        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
962        let new_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
963        self.cursor = new_cursor;
964
965        let range = before_cursor..new_cursor;
966        let op = BufferOp::Replace {
967            range,
968            text,
969            old_text: String::new(),
970            end_position: new_cursor,
971        };
972
973        let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
974
975        self.apply_change(change, ChangeKind::Structural);
976        self.increment_version();
977    }
978
979    pub fn delete_word_backward(&mut self) {
980        let end_byte = self.cursor_to_byte();
981        if end_byte == 0 {
982            return;
983        }
984
985        let before_cursor = self.cursor;
986        let before_selection = self.selection;
987
988        // Use the same token logic as `word_boundary_prev` so deletion mirrors movement.
989        let start_pos = self.word_boundary_prev(self.cursor);
990        let start_byte = self.pos_to_byte(start_pos);
991
992        if start_byte < end_byte {
993            let deleted_text: String = self.text.slice(start_byte..end_byte).into();
994            self.text.remove(start_byte..end_byte);
995            let new_pos = self.byte_to_pos(start_byte);
996            self.cursor = new_pos;
997
998            let range = new_pos..before_cursor;
999            let op = BufferOp::Replace {
1000                range,
1001                text: String::new(),
1002                old_text: deleted_text,
1003                end_position: new_pos,
1004            };
1005
1006            let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
1007
1008            self.apply_change(change, ChangeKind::DeleteText);
1009            self.increment_version();
1010        }
1011    }
1012
1013    pub fn delete_word_forward(&mut self) {
1014        let start = self.cursor_to_byte();
1015        if start >= self.text.len() {
1016            return;
1017        }
1018
1019        let before_cursor = self.cursor;
1020        let before_selection = self.selection;
1021
1022        let mut char_idx = self.text.byte_to_char_idx(start);
1023        let end_char = self.text.len_chars();
1024
1025        let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
1026        let at_whitespace = first_char.is_whitespace();
1027
1028        while char_idx < end_char
1029            && self
1030                .text
1031                .char(self.text.char_to_byte_idx(char_idx))
1032                .is_whitespace()
1033        {
1034            char_idx += 1;
1035        }
1036
1037        if !at_whitespace {
1038            while char_idx < end_char {
1039                let curr_byte = self.text.char_to_byte_idx(char_idx);
1040                if self.text.char(curr_byte).is_whitespace() {
1041                    break;
1042                }
1043                char_idx += 1;
1044            }
1045
1046            while char_idx < end_char
1047                && self
1048                    .text
1049                    .char(self.text.char_to_byte_idx(char_idx))
1050                    .is_whitespace()
1051            {
1052                char_idx += 1;
1053            }
1054        }
1055
1056        let end_byte = if char_idx < end_char {
1057            self.text.char_to_byte_idx(char_idx)
1058        } else {
1059            self.text.len()
1060        };
1061        let start_byte = self
1062            .text
1063            .char_to_byte_idx(self.text.byte_to_char_idx(start));
1064
1065        if start_byte < end_byte {
1066            let deleted_text: String = self.text.slice(start_byte..end_byte).into();
1067            self.text.remove(start_byte..end_byte);
1068
1069            let range = before_cursor..before_cursor;
1070            let op = BufferOp::Replace {
1071                range,
1072                text: String::new(),
1073                old_text: deleted_text,
1074                end_position: before_cursor,
1075            };
1076
1077            let change = ChangeSet::new(
1078                vec![op],
1079                before_cursor,
1080                before_cursor,
1081                before_selection,
1082                None,
1083            );
1084
1085            self.apply_change(change, ChangeKind::DeleteText);
1086            self.increment_version();
1087        }
1088    }
1089
1090    pub fn word_range_at(&self, pos: Position) -> (usize, usize) {
1091        let byte_idx = self.pos_to_byte(pos);
1092        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1093        let line = self.text.line(pos.line, self.line_type());
1094
1095        let mut word_start = 0;
1096        let mut word_end = line.len_chars();
1097        let mut in_word = false;
1098
1099        for (i, (byte_offset, ch)) in line.char_indices().enumerate() {
1100            let abs_byte = line_start_byte + byte_offset;
1101            if abs_byte >= byte_idx && ch.is_alphanumeric() && !in_word {
1102                word_start = i;
1103                in_word = true;
1104            }
1105            if in_word && !ch.is_alphanumeric() {
1106                word_end = i;
1107                break;
1108            }
1109        }
1110
1111        (word_start, word_end)
1112    }
1113
1114    pub fn insert_at(&mut self, pos: Position, text: &str) {
1115        let before_cursor = self.cursor;
1116        let before_selection = self.selection;
1117
1118        let byte_idx = self.pos_to_byte(pos);
1119        self.text.insert(byte_idx, text);
1120
1121        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
1122        let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
1123        if pos == before_cursor {
1124            self.cursor = after_cursor;
1125        }
1126
1127        let range = pos..after_cursor;
1128        let op = BufferOp::Replace {
1129            range,
1130            text: text.to_string(),
1131            old_text: String::new(),
1132            end_position: after_cursor,
1133        };
1134
1135        let change = ChangeSet::new(
1136            vec![op],
1137            before_cursor,
1138            after_cursor,
1139            before_selection,
1140            None,
1141        );
1142
1143        self.apply_change(change, ChangeKind::InsertText);
1144        self.increment_version();
1145    }
1146
1147    pub fn insert_text_raw(&mut self, text: &str) {
1148        let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1149        self.insert(&normalized);
1150    }
1151
1152    pub fn delete_line(&mut self) -> String {
1153        let line_idx = self.cursor.line;
1154        if line_idx >= self.text.len_lines(self.line_type()) {
1155            return String::new();
1156        }
1157
1158        let line_start = self.text.line_to_byte_idx(line_idx, self.line_type());
1159        let line_end = if line_idx + 1 < self.text.len_lines(self.line_type()) {
1160            self.text.line_to_byte_idx(line_idx + 1, self.line_type())
1161        } else {
1162            self.text.len()
1163        };
1164
1165        let deleted: String = self.text.slice(line_start..line_end).into();
1166
1167        let before_cursor = self.cursor;
1168        let before_selection = self.selection;
1169
1170        self.text.remove(line_start..line_end);
1171
1172        if self.text.len_lines(self.line_type()) == 0 {
1173            self.text.insert(0, "");
1174        }
1175
1176        let new_line_idx = line_idx.min(self.text.len_lines(self.line_type()).saturating_sub(1));
1177        let new_pos = Position::new(new_line_idx, 0);
1178        self.cursor = new_pos;
1179        self.normalize_cursor();
1180
1181        let range = new_pos..before_cursor;
1182        let op = BufferOp::Replace {
1183            range,
1184            text: String::new(),
1185            old_text: deleted.clone(),
1186            end_position: new_pos,
1187        };
1188
1189        let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
1190
1191        self.apply_change(change, ChangeKind::DeleteText);
1192        self.increment_version();
1193
1194        deleted
1195    }
1196
1197    pub fn selected_text_all(&self) -> String {
1198        self.selected_text()
1199    }
1200
1201    pub fn cursor_left(&mut self) -> Position {
1202        let pos = self.offset_left(self.cursor);
1203        self.cursor = pos;
1204        self.cursor
1205    }
1206
1207    pub fn offset_left(&self, pos: Position) -> Position {
1208        if pos.column > 0 {
1209            Position::new(pos.line, pos.column - 1)
1210        } else if pos.line > 0 {
1211            let prev_line = pos.line - 1;
1212            let prev_line_len = self.line_display_len(prev_line);
1213            Position::new(prev_line, prev_line_len)
1214        } else {
1215            pos
1216        }
1217    }
1218
1219    pub fn cursor_right(&mut self) -> Position {
1220        let pos = self.offset_right(self.cursor);
1221        self.cursor = pos;
1222        self.cursor
1223    }
1224
1225    pub fn offset_right(&self, pos: Position) -> Position {
1226        let line_len = self.line_display_len(pos.line);
1227        if pos.column < line_len {
1228            Position::new(pos.line, pos.column + 1)
1229        } else if pos.line < self.text.len_lines(self.line_type()) - 1 {
1230            Position::new(pos.line + 1, 0)
1231        } else {
1232            pos
1233        }
1234    }
1235
1236    /// Apply a cursor movement, updating self.cursor to the new position.
1237    fn apply_movement(&mut self, new_pos: Position) -> Position {
1238        self.cursor = new_pos;
1239        self.cursor
1240    }
1241
1242    pub fn cursor_up(&mut self) -> Position {
1243        self.apply_movement(self.offset_up(self.cursor))
1244    }
1245
1246    pub fn offset_up(&self, pos: Position) -> Position {
1247        if pos.line == 0 { pos } else { self.offset_up_n(pos, 1) }
1248    }
1249
1250    pub fn cursor_down(&mut self) -> Position {
1251        self.apply_movement(self.offset_down(self.cursor))
1252    }
1253
1254    pub fn offset_down(&self, pos: Position) -> Position {
1255        if pos.line >= self.text.len_lines(self.line_type()).saturating_sub(1) {
1256            pos
1257        } else {
1258            self.offset_down_n(pos, 1)
1259        }
1260    }
1261
1262    pub fn cursor_up_n(&mut self, n: usize) -> Position {
1263        self.apply_movement(self.offset_up_n(self.cursor, n))
1264    }
1265
1266    pub fn offset_up_n(&self, pos: Position, n: usize) -> Position {
1267        let new_line = pos.line.saturating_sub(n);
1268        let line_len = self.line_display_len(new_line);
1269        Position::new(new_line, pos.column.min(line_len))
1270    }
1271
1272    pub fn cursor_down_n(&mut self, n: usize) -> Position {
1273        self.apply_movement(self.offset_down_n(self.cursor, n))
1274    }
1275
1276    pub fn offset_down_n(&self, pos: Position, n: usize) -> Position {
1277        let new_line = (pos.line + n).min(self.text.len_lines(self.line_type()).saturating_sub(1));
1278        let line_len = self.line_display_len(new_line);
1279        Position::new(new_line, pos.column.min(line_len))
1280    }
1281
1282    pub fn cursor_page_up(&mut self, height: usize) -> Position {
1283        self.apply_movement(self.offset_up_n(self.cursor, height))
1284    }
1285
1286    pub fn cursor_page_down(&mut self, height: usize) -> Position {
1287        self.apply_movement(self.offset_down_n(self.cursor, height))
1288    }
1289
1290    pub fn cursor_line_start(&mut self) -> Position {
1291        self.cursor.column = 0;
1292        self.cursor
1293    }
1294
1295    pub fn offset_line_start(&self, pos: Position) -> Position {
1296        Position::new(pos.line, 0)
1297    }
1298
1299    pub fn cursor_line_end(&mut self) -> Position {
1300        self.cursor.column = self.line_display_len(self.cursor.line);
1301        self.cursor
1302    }
1303
1304    pub fn offset_line_end(&self, pos: Position) -> Position {
1305        Position::new(pos.line, self.line_display_len(pos.line))
1306    }
1307
1308    pub fn cursor_word_prev(&mut self) -> Position {
1309        self.apply_movement(self.word_boundary_prev(self.cursor))
1310    }
1311
1312    pub fn cursor_word_next(&mut self) -> Position {
1313        self.apply_movement(self.word_boundary_next_for_selection(self.cursor))
1314    }
1315
1316    pub fn word_boundary_prev(&self, pos: Position) -> Position {
1317        // If at logical start of line (column 0 or first non-whitespace), move to end of previous line (do not attempt token logic).
1318        if pos.line > 0 {
1319            let line_slice = self.text.line(pos.line, self.line_type());
1320            let mut first_non_ws_col: Option<usize> = None;
1321            for (i, c) in line_slice.chars().enumerate() {
1322                if !c.is_whitespace() || c == '\n' {
1323                    first_non_ws_col = Some(i);
1324                    break;
1325                }
1326            }
1327            if pos.column == 0 || first_non_ws_col == Some(pos.column) {
1328                let prev_line = pos.line - 1;
1329                return Position::new(prev_line, self.line_display_len(prev_line));
1330            }
1331        }
1332
1333        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1334        let byte_idx = line_start_byte + self.text.char_to_byte_idx(pos.column);
1335
1336        if byte_idx == 0 {
1337            return pos;
1338        }
1339
1340        // Start from the character immediately before the cursor.
1341        let mut char_idx = self.text.byte_to_char_idx(byte_idx);
1342        if char_idx == 0 {
1343            return pos;
1344        }
1345        char_idx = char_idx.saturating_sub(1);
1346
1347        // Phase 1: skip whitespace (but not newlines) immediately before the cursor.
1348        while char_idx > 0 {
1349            let c_byte = self.text.char_to_byte_idx(char_idx);
1350            let c = self.text.char(c_byte);
1351            if !c.is_whitespace() || c == '\n' {
1352                break;
1353            }
1354            char_idx = char_idx.saturating_sub(1);
1355        }
1356
1357        // Determine token type: punctuation vs other (word) characters.
1358        let c_byte = self.text.char_to_byte_idx(char_idx);
1359        let first_char = self.text.char(c_byte);
1360        let is_punct = first_char.is_ascii_punctuation();
1361
1362        // Phase 2: move left over the token of the same type.
1363        while char_idx > 0 {
1364            let prev_byte = self.text.char_to_byte_idx(char_idx.saturating_sub(1));
1365            let prev_char = self.text.char(prev_byte);
1366            if is_punct {
1367                if !prev_char.is_ascii_punctuation() {
1368                    break;
1369                }
1370            } else if prev_char.is_whitespace() || prev_char.is_ascii_punctuation() {
1371                break;
1372            }
1373            char_idx = char_idx.saturating_sub(1);
1374        }
1375
1376        self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
1377    }
1378
1379    pub fn word_boundary_next(&self, pos: Position) -> Position {
1380        self.word_boundary_next_impl(pos, true)
1381    }
1382
1383    pub fn word_boundary_next_for_selection(&self, pos: Position) -> Position {
1384        self.word_boundary_next_impl(pos, false)
1385    }
1386
1387    fn word_boundary_next_impl(&self, mut pos: Position, skip_whitespace_after: bool) -> Position {
1388        let end_char = self.text.len_chars();
1389
1390        // Precompute line-local char bounds so we can clamp to end-of-line and
1391        // avoid crossing to the next line.
1392        let mut line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1393        let mut line_start_char = self.text.byte_to_char_idx(line_start_byte);
1394        let mut line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
1395        let line_slice = self.text.line(pos.line, self.line_type());
1396        // Printable length excludes the trailing newline if present (rope lines include the newline
1397        // on all but the last line). Use printable_line_len for 'visual' end-of-line semantics.
1398        let mut printable_line_len = if line_slice.chars().last() == Some('\n') {
1399            line_len_chars.saturating_sub(1)
1400        } else {
1401            line_len_chars
1402        };
1403        let mut printable_line_end_char_idx = line_start_char + printable_line_len;
1404
1405        let mut char_idx = self.pos_to_char(pos);
1406
1407        // If at or beyond EOF, stay put.
1408        if char_idx >= end_char {
1409            return pos;
1410        }
1411
1412
1413        // If already at or beyond end-of-line (visual EOL), attempt to move to the first
1414        // token on the next line instead of clamping here.
1415        let total_lines = self.text.len_lines(self.line_type());
1416        if char_idx >= printable_line_end_char_idx {
1417            if pos.line + 1 >= total_lines {
1418                // No next line, clamp to current line printable end.
1419                return Position::new(pos.line, printable_line_len);
1420            }
1421
1422            // Move to the start of the next line and recompute bounds.
1423            pos = Position::new(pos.line + 1, 0);
1424            line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1425            line_start_char = self.text.byte_to_char_idx(line_start_byte);
1426            line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
1427            let line_slice = self.text.line(pos.line, self.line_type());
1428            printable_line_len = if line_slice.chars().last() == Some('\n') {
1429                line_len_chars.saturating_sub(1)
1430            } else {
1431                line_len_chars
1432            };
1433            printable_line_end_char_idx = line_start_char + printable_line_len;
1434            // Start at the beginning of the new line and move to the first non-whitespace
1435            // char (the start of the first word). This is the desired behavior when
1436            // ctrl+right is pressed at the visual end-of-line.
1437            let mut next_char_idx = self.pos_to_char(pos);
1438            while next_char_idx < end_char
1439                && self
1440                    .text
1441                    .char(self.text.char_to_byte_idx(next_char_idx))
1442                    .is_whitespace()
1443            {
1444                next_char_idx += 1;
1445                if next_char_idx >= printable_line_end_char_idx {
1446                    return Position::new(pos.line, printable_line_len);
1447                }
1448            }
1449            if next_char_idx >= end_char {
1450                return pos;
1451            }
1452            return self.byte_to_pos(self.text.char_to_byte_idx(next_char_idx));
1453        }
1454
1455        // Phase 1: skip whitespace starting at the current char. If skipping would
1456        // cross the line boundary, clamp to end-of-line instead of crossing.
1457        while char_idx < end_char
1458            && self
1459                .text
1460                .char(self.text.char_to_byte_idx(char_idx))
1461                .is_whitespace()
1462        {
1463            char_idx += 1;
1464            if char_idx >= printable_line_end_char_idx {
1465                return Position::new(pos.line, printable_line_len);
1466            }
1467        }
1468
1469        if char_idx >= end_char {
1470            return pos;
1471        }
1472
1473        // Identify token type at the current position.
1474        let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
1475        let is_punct = first_char.is_ascii_punctuation();
1476
1477        // Phase 2: advance over the token. If advancing reaches or crosses the
1478        // line end, clamp to end-of-line.
1479        while char_idx < end_char {
1480            let c = self.text.char(self.text.char_to_byte_idx(char_idx));
1481            if is_punct {
1482                if !c.is_ascii_punctuation() {
1483                    break;
1484                }
1485            } else if c.is_whitespace() || c.is_ascii_punctuation() {
1486                break;
1487            }
1488            char_idx += 1;
1489            if char_idx >= printable_line_end_char_idx {
1490                return Position::new(pos.line, printable_line_len);
1491            }
1492        }
1493
1494        if skip_whitespace_after {
1495            while char_idx < end_char
1496                && self
1497                    .text
1498                    .char(self.text.char_to_byte_idx(char_idx))
1499                    .is_whitespace()
1500            {
1501                char_idx += 1;
1502                if char_idx >= printable_line_end_char_idx {
1503                    return Position::new(pos.line, printable_line_len);
1504                }
1505            }
1506        }
1507
1508        // If we've advanced past the line end due to some edge-case, clamp.
1509        if char_idx >= printable_line_end_char_idx {
1510            return Position::new(pos.line, printable_line_len);
1511        }
1512
1513        self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
1514    }
1515
1516    pub fn select_all(&mut self) {
1517        let last_line = self.text.len_lines(self.line_type()).saturating_sub(1);
1518        let pos = Position::new(
1519            last_line,
1520            self.text.line(last_line, self.line_type()).len_chars(),
1521        );
1522        self.selection = Some(Selection::new(Position::new(0, 0), pos));
1523        self.cursor = pos;
1524    }
1525
1526    pub fn selected_text(&self) -> String {
1527        if let Some(sel) = self.selection {
1528            let start = self.pos_to_byte(sel.min());
1529            let end = self.pos_to_byte(sel.max());
1530            self.text.slice(start..end).into()
1531        } else {
1532            String::new()
1533        }
1534    }
1535
1536    pub fn delete_selection(&mut self) -> String {
1537        if let Some(sel) = self.selection {
1538            let start = sel.min();
1539            let end = sel.max();
1540            let text = self.slice(start..end);
1541            self.delete_range(start, end);
1542            text
1543        } else {
1544            String::new()
1545        }
1546    }
1547
1548    pub fn undo(&mut self) -> bool {
1549        let inverse = {
1550            match self.history.undo() {
1551                Some(node) => node.inverse.clone(),
1552                None => return false,
1553            }
1554        };
1555        apply_changeset(self, &inverse);
1556        true
1557    }
1558
1559    pub fn redo(&mut self) -> bool {
1560        let change = {
1561            match self.history.redo() {
1562                Some(node) => node.change.clone(),
1563                None => return false,
1564            }
1565        };
1566        apply_changeset(self, &change);
1567        true
1568    }
1569
1570    pub fn undo_stack(&self) -> Vec<SerializableSnapshot> {
1571        Vec::new()
1572    }
1573
1574    pub fn redo_stack(&self) -> Vec<SerializableSnapshot> {
1575        Vec::new()
1576    }
1577
1578    pub fn save(&mut self, settings: &crate::settings::Settings) -> Result<()> {
1579        let path = self
1580            .path
1581            .clone()
1582            .ok_or_else(|| anyhow!("No path associated with buffer"))?;
1583
1584        self.is_saving = true;
1585        let result = self.do_save(&path, settings);
1586        self.is_saving = false;
1587        result
1588    }
1589
1590    fn do_save(&mut self, path: &PathBuf, settings: &crate::settings::Settings) -> Result<()> {
1591        let should_trim = *adapters::editor::trim_on_save(settings);
1592
1593        let mut text = self.text.clone();
1594
1595        if should_trim {
1596            let content = text.to_string();
1597            let trimmed: String = content
1598                .lines()
1599                .map(|l| l.trim_end().to_string())
1600                .collect::<Vec<_>>()
1601                .join("\n");
1602
1603            let trailing_empty = trimmed.lines().rev().take_while(|l| l.is_empty()).count();
1604            let final_lines: String = if trailing_empty > 1 {
1605                trimmed
1606                    .lines()
1607                    .take(trimmed.lines().count() - (trailing_empty - 1))
1608                    .collect::<Vec<_>>()
1609                    .join("\n")
1610            } else {
1611                trimmed.clone()
1612            };
1613
1614            text = Rope::from_str(&final_lines);
1615
1616            if !final_lines.ends_with('\n') && !final_lines.is_empty() {
1617                text.insert(text.len(), "\n");
1618            }
1619
1620            self.text = text.clone();
1621            self.normalize_cursor();
1622        }
1623
1624        let content = if text.len_lines(self.line_type()) > 0 {
1625            let last_line = text.len_lines(self.line_type()) - 1;
1626            let last_line_len = text.line(last_line, self.line_type()).len_chars();
1627            if last_line_len == 0 {
1628                text.to_string().trim_end_matches('\n').to_string()
1629            } else {
1630                text.to_string()
1631            }
1632        } else {
1633            text.to_string()
1634        };
1635
1636        let tmp = path.with_extension("tmp");
1637        fs::write(&tmp, &content)?;
1638        fs::rename(&tmp, path).or_else(|_| {
1639            fs::remove_file(path)?;
1640            fs::rename(&tmp, path)
1641        })?;
1642
1643        self.file_hash = Self::compute_hash(&text);
1644        self.dirty = false;
1645        if let Some((mtime, size)) = Self::get_file_metadata(path) {
1646            self.file_mtime = Some(mtime);
1647            self.file_size = Some(size);
1648        }
1649        Ok(())
1650    }
1651
1652    pub fn compute_hash(text: &Rope) -> Option<u64> {
1653        use std::collections::hash_map::DefaultHasher;
1654        use std::hash::{Hash, Hasher};
1655        let mut hasher = DefaultHasher::new();
1656        text.hash(&mut hasher);
1657        Some(hasher.finish())
1658    }
1659
1660    pub fn compute_file_hash(path: &Path) -> Option<u64> {
1661        use std::collections::hash_map::DefaultHasher;
1662        use std::hash::{Hash, Hasher};
1663        use std::io::Read;
1664        let mut file = fs::File::open(path).ok()?;
1665        let mut contents = Vec::new();
1666        file.read_to_end(&mut contents).ok()?;
1667        let mut hasher = DefaultHasher::new();
1668        contents.hash(&mut hasher);
1669        Some(hasher.finish())
1670    }
1671
1672    pub fn get_file_metadata(path: &Path) -> Option<(u64, u64)> {
1673        let metadata = fs::metadata(path).ok()?;
1674        let mtime = metadata
1675            .modified()
1676            .ok()?
1677            .duration_since(std::time::UNIX_EPOCH)
1678            .ok()?
1679            .as_secs();
1680        let size = metadata.len();
1681        Some((mtime, size))
1682    }
1683
1684    pub fn compute_and_store_file_hash(&mut self) {
1685        if let Some(ref path) = self.path {
1686            self.file_hash = Self::compute_file_hash(path);
1687            if let Some((mtime, size)) = Self::get_file_metadata(path) {
1688                self.file_mtime = Some(mtime);
1689                self.file_size = Some(size);
1690            }
1691        }
1692    }
1693
1694    pub fn check_external_modification(&mut self) -> bool {
1695        if self.is_saving {
1696            return false;
1697        }
1698        if self.is_dirty() {
1699            return false;
1700        }
1701        if let Some(ref path) = self.path
1702            && let Some((current_mtime, current_size)) = Self::get_file_metadata(path) {
1703                let mtime_changed = self.file_mtime.map(|m| m != current_mtime).unwrap_or(false);
1704                let size_changed = self.file_size.map(|s| s != current_size).unwrap_or(false);
1705                if !mtime_changed && !size_changed {
1706                    return false;
1707                }
1708                if let Some(current_hash) = Self::compute_file_hash(path)
1709                    && let Some(saved_hash) = self.file_hash
1710                        && current_hash != saved_hash {
1711                            self.external_modification_detected = true;
1712                            return true;
1713                        }
1714            }
1715        false
1716    }
1717
1718    pub fn reload_from_disk(&mut self) -> Result<()> {
1719        let path = self
1720            .path
1721            .clone()
1722            .ok_or_else(|| anyhow!("No path associated with buffer"))?;
1723        let content = fs::read_to_string(&path)?;
1724        self.text = Rope::from_str(&content);
1725        self.file_hash = Self::compute_hash(&self.text);
1726        if let Some((mtime, size)) = Self::get_file_metadata(&path) {
1727            self.file_mtime = Some(mtime);
1728            self.file_size = Some(size);
1729        }
1730        self.external_modification_detected = false;
1731        self.normalize_cursor();
1732        Ok(())
1733    }
1734
1735    pub fn clear_external_modification(&mut self) {
1736        self.external_modification_detected = false;
1737    }
1738
1739    pub fn scroll_to_cursor(&mut self, height: usize) {
1740        if height == 0 {
1741            return;
1742        }
1743        if self.cursor.line < self.scroll {
1744            self.scroll = self.cursor.line;
1745        } else if self.cursor.line >= self.scroll + height {
1746            self.scroll = self.cursor.line + 1 - height;
1747        }
1748    }
1749
1750    pub fn scroll_to_cursor_visual(
1751        &mut self,
1752        height: usize,
1753        _content_width: usize,
1754        _indent_width: usize,
1755    ) {
1756        self.scroll_to_cursor(height);
1757    }
1758
1759    pub fn scroll_x_to_cursor(&mut self, _content_width: usize, _indent_width: usize) {}
1760
1761    pub fn slice(&self, range: Range<Position>) -> String {
1762        let start_byte = self.pos_to_byte(range.start);
1763        let end_byte = self.pos_to_byte(range.end);
1764        self.text.slice(start_byte..end_byte).into()
1765    }
1766
1767    pub fn text(&self) -> String {
1768        self.text.to_string()
1769    }
1770
1771    pub fn apply_op_without_history(&mut self, op: &BufferOp) {
1772        match op {
1773            BufferOp::Replace { range, text, .. } => {
1774                self.replace_range_direct(range.start, range.end, text);
1775            }
1776            BufferOp::MoveCursor { position } => {
1777                self.set_cursor(*position);
1778            }
1779            BufferOp::SetSelection { selection } => {
1780                self.set_selection(*selection);
1781            }
1782        }
1783    }
1784
1785    fn replace_range_direct(&mut self, start: Position, end: Position, text: &str) {
1786        let start_byte = self.pos_to_byte(start);
1787        let end_byte = self.pos_to_byte(end).min(self.text.len());
1788
1789        if start_byte < end_byte {
1790            self.text.remove(start_byte..end_byte);
1791        }
1792        self.text.insert(start_byte, text);
1793    }
1794}
1795
1796impl Default for Buffer {
1797    fn default() -> Self {
1798        Self::new()
1799    }
1800}
1801
1802impl From<&Rope> for Buffer {
1803    fn from(rope: &Rope) -> Self {
1804        let mut buf = Self::new();
1805        buf.text = rope.clone();
1806        buf
1807    }
1808}
1809
1810#[cfg(test)]
1811mod tests {
1812    use super::*;
1813
1814    #[test]
1815    fn test_position_mapping() {
1816        let buf = Buffer::from_text("hello\nworld\n");
1817
1818        assert_eq!(buf.byte_to_pos(0), Position::new(0, 0));
1819        assert_eq!(buf.byte_to_pos(5), Position::new(0, 5));
1820        assert_eq!(buf.byte_to_pos(6), Position::new(1, 0));
1821        assert_eq!(buf.byte_to_pos(11), Position::new(1, 5));
1822
1823        assert_eq!(buf.pos_to_byte(Position::new(0, 0)), 0);
1824        assert_eq!(buf.pos_to_byte(Position::new(0, 5)), 5);
1825        assert_eq!(buf.pos_to_byte(Position::new(1, 0)), 6);
1826        assert_eq!(buf.pos_to_byte(Position::new(1, 5)), 11);
1827    }
1828
1829    #[test]
1830    fn test_insert() {
1831        let mut buf = Buffer::from_text("hello");
1832        buf.cursor = Position::new(0, 5);
1833        buf.insert(" world");
1834        assert_eq!(buf.text.to_string(), "hello world");
1835    }
1836
1837    #[test]
1838    fn test_insert_newline() {
1839        let mut buf = Buffer::from_text("hello");
1840        buf.cursor = Position::new(0, 5);
1841        buf.insert_newline();
1842        assert_eq!(buf.text.to_string(), "hello\n");
1843        assert_eq!(buf.cursor, Position::new(1, 0));
1844    }
1845
1846    #[test]
1847    fn test_delete_backward() {
1848        let mut buf = Buffer::from_text("hello world");
1849        buf.cursor = Position::new(0, 5);
1850        buf.delete_backward();
1851        assert_eq!(buf.text.to_string(), "hell world");
1852        assert_eq!(buf.cursor, Position::new(0, 4));
1853    }
1854
1855    #[test]
1856    fn test_delete_selection() {
1857        let mut buf = Buffer::from_text("hello world");
1858        buf.selection = Some(Selection::new(Position::new(0, 0), Position::new(0, 6)));
1859        buf.delete_selection();
1860        assert_eq!(buf.text.to_string(), "world");
1861    }
1862
1863    #[test]
1864    fn test_cursor_movement() {
1865        let mut buf = Buffer::from_text("hello\nworld");
1866
1867        buf.cursor = Position::new(0, 3);
1868        assert_eq!(buf.cursor_right(), Position::new(0, 4));
1869        assert_eq!(buf.cursor_left(), Position::new(0, 3));
1870
1871        buf.cursor = Position::new(0, 0);
1872        assert_eq!(buf.cursor_up(), Position::new(0, 0));
1873
1874        buf.cursor = Position::new(0, 3);
1875        buf.cursor_up();
1876        assert_eq!(buf.cursor.line, 0);
1877
1878        buf.cursor = Position::new(1, 3);
1879        buf.cursor_up();
1880        assert_eq!(buf.cursor.line, 0);
1881        assert_eq!(buf.cursor.column, 3);
1882    }
1883
1884    #[test]
1885    fn test_undo_multiple_steps() {
1886        let mut buf = Buffer::from_text("");
1887
1888        buf.insert("a");
1889        buf.insert("b");
1890        buf.insert("c");
1891
1892        assert_eq!(buf.text(), "abc");
1893        // fast consecutive inserts of same kind coalesce into one entry
1894        assert_eq!(buf.history_position(), 1);
1895
1896        assert!(buf.undo());
1897        assert_eq!(buf.text(), "");
1898
1899        assert!(!buf.undo());
1900    }
1901
1902    #[test]
1903    fn test_undo_redo_multiple_steps() {
1904        let mut buf = Buffer::from_text("");
1905
1906        buf.insert("a");
1907        buf.insert("b");
1908
1909        assert_eq!(buf.text(), "ab");
1910        assert_eq!(buf.history_position(), 1);
1911
1912        buf.undo();
1913        assert_eq!(buf.text(), "");
1914
1915        buf.redo();
1916        assert_eq!(buf.text(), "ab");
1917
1918        assert!(!buf.redo());
1919    }
1920
1921    #[test]
1922    fn test_undo_after_new_edit() {
1923        let mut buf = Buffer::from_text("");
1924
1925        buf.insert("a");
1926        buf.insert("b");
1927
1928        // "a" and "b" merge → one undo clears both
1929        buf.undo();
1930        assert_eq!(buf.text(), "");
1931
1932        buf.insert("c");
1933        assert_eq!(buf.text(), "c");
1934
1935        buf.undo();
1936        assert_eq!(buf.text(), "");
1937    }
1938
1939    #[test]
1940    fn test_line_access() {
1941        let buf = Buffer::from_text("hello\nworld\n");
1942
1943        assert_eq!(buf.line_count(), 3);
1944        assert_eq!(buf.line(0).unwrap().to_string(), "hello");
1945        assert_eq!(buf.line(1).unwrap().to_string(), "world");
1946    }
1947
1948    #[test]
1949    fn test_cursor_clamping() {
1950        let mut buf = Buffer::from_text("hello");
1951        buf.cursor = Position::new(100, 100);
1952        buf.normalize_cursor();
1953
1954        assert!(buf.cursor.line < buf.line_count());
1955        assert!(buf.cursor.column <= buf.line(0).unwrap().chars().count());
1956    }
1957
1958    #[test]
1959    fn test_unicode() {
1960        let mut buf = Buffer::from_text("héllo\nwörld");
1961        assert_eq!(buf.line_count(), 2);
1962
1963        buf.cursor = Position::new(0, 6);
1964        buf.insert("!");
1965        let line0 = buf.text.line(0, LineType::LF).to_string();
1966        assert!(
1967            line0.starts_with("héllo"),
1968            "line0 should start with héllo, got: {}",
1969            line0
1970        );
1971    }
1972
1973    #[test]
1974    fn test_snapshot() {
1975        let buf = Buffer::from_text("hello");
1976        let snap = buf.snapshot();
1977        assert_eq!(snap.text.to_string(), "hello");
1978    }
1979
1980    #[test]
1981    fn test_delete_forward() {
1982        let mut buf = Buffer::from_text("hello");
1983        buf.cursor = Position::new(0, 4);
1984        buf.delete_forward();
1985        assert_eq!(buf.text.to_string(), "hell");
1986    }
1987
1988    #[test]
1989    fn test_empty_buffer() {
1990        let buf = Buffer::new();
1991        assert!(buf.is_empty());
1992        assert_eq!(buf.line_count(), 1);
1993        assert_eq!(buf.cursor, Position::new(0, 0));
1994    }
1995
1996    #[test]
1997    fn test_buffer_id() {
1998        let buf1 = Buffer::new();
1999        let buf2 = Buffer::new();
2000        assert_ne!(buf1.id(), buf2.id());
2001    }
2002
2003    #[test]
2004    fn test_version_increment() {
2005        let mut buf = Buffer::from_text("hello");
2006        let v1 = buf.version();
2007        buf.insert(" world");
2008        let v2 = buf.version();
2009        assert!(v2 > v1);
2010    }
2011
2012    #[test]
2013    fn test_fast_typing_merges_into_single_undo() {
2014        let mut buf = Buffer::from_text("");
2015
2016        buf.insert("h");
2017        buf.insert("e");
2018        buf.insert("l");
2019        buf.insert("l");
2020        buf.insert("o");
2021
2022        assert_eq!(buf.text(), "hello");
2023        // all five chars are adjacent and fast → one history entry
2024        assert_eq!(buf.history_position(), 1);
2025
2026        buf.undo();
2027        assert_eq!(buf.text(), "");
2028    }
2029
2030    #[test]
2031    fn test_fast_backspace_merges_into_single_undo() {
2032        let mut buf = Buffer::from_text("hello");
2033        buf.set_cursor(Position::new(0, 5));
2034
2035        buf.delete_backward();
2036        buf.delete_backward();
2037        buf.delete_backward();
2038
2039        assert_eq!(buf.text(), "he");
2040        assert_eq!(buf.history_position(), 1);
2041
2042        buf.undo();
2043        assert_eq!(buf.text(), "hello");
2044    }
2045
2046    #[test]
2047    fn test_structural_ops_break_merge_chain() {
2048        let mut buf = Buffer::from_text("");
2049
2050        buf.insert("a");
2051        buf.insert("b"); // merges with "a" → 1 entry
2052        buf.insert_newline(); // Structural → new entry
2053        buf.insert("c"); // InsertText → new entry (different from Structural)
2054
2055        assert_eq!(buf.text(), "ab\nc");
2056        // "ab" (merged), newline, "c" → 3 entries
2057        assert_eq!(buf.history_position(), 3);
2058
2059        buf.undo();
2060        assert_eq!(buf.text(), "ab\n");
2061
2062        buf.undo();
2063        assert_eq!(buf.text(), "ab");
2064
2065        buf.undo();
2066        assert_eq!(buf.text(), "");
2067        buf.undo();
2068        assert_eq!(buf.text(), "");
2069    }
2070
2071    #[test]
2072    fn test_mixed_undo_steps() {
2073        let mut buf = Buffer::from_text("");
2074
2075        buf.insert("hello");
2076        buf.insert_newline();
2077        buf.insert("world");
2078
2079        buf.undo();
2080        assert_eq!(buf.text(), "hello\n");
2081
2082        buf.undo();
2083        assert_eq!(buf.text(), "hello");
2084
2085        buf.undo();
2086        assert_eq!(buf.text(), "");
2087    }
2088
2089    #[test]
2090    fn test_cross_line_insert_over_selection() {
2091        let mut buf = Buffer::from_text("hello\nworld");
2092        buf.set_selection(Some(Selection::new(
2093            Position::new(0, 2),
2094            Position::new(1, 3),
2095        )));
2096        buf.insert("X");
2097        assert_eq!(buf.text(), "heXld");
2098    }
2099
2100    #[test]
2101    fn test_cross_line_delete_backward_with_selection() {
2102        let mut buf = Buffer::from_text("hello\nworld");
2103        buf.set_selection(Some(Selection::new(
2104            Position::new(0, 5),
2105            Position::new(1, 0),
2106        )));
2107        buf.delete_backward();
2108        assert_eq!(buf.text(), "helloworld");
2109    }
2110
2111    #[test]
2112    fn test_cross_line_selection_delete_all() {
2113        let mut buf = Buffer::from_text("abc\ndef\nghi");
2114        buf.set_selection(Some(Selection::new(
2115            Position::new(0, 0),
2116            Position::new(2, 3),
2117        )));
2118        buf.insert("X");
2119        assert_eq!(buf.text(), "X");
2120    }
2121
2122    #[test]
2123    fn test_cross_line_selection_undo_redo() {
2124        let mut buf = Buffer::from_text("hello\nworld");
2125        // Selection: anchor=(0,3), active=(1,2), cursor=(1,2)
2126        buf.set_selection(Some(Selection::new(
2127            Position::new(0, 3),
2128            Position::new(1, 2),
2129        )));
2130        buf.insert("X");
2131        assert_eq!(buf.text(), "helXrld");
2132        assert_eq!(buf.cursor(), Position::new(0, 4)); // after "helX"
2133        buf.undo();
2134        assert_eq!(buf.text(), "hello\nworld");
2135        // Undo restores cursor to before_cursor = (1,2) (active end of original selection)
2136        assert_eq!(buf.cursor(), Position::new(1, 2));
2137        buf.redo();
2138        assert_eq!(buf.text(), "helXrld");
2139        assert_eq!(buf.cursor(), Position::new(0, 4));
2140    }
2141
2142    #[test]
2143    fn test_offset_up_excludes_trailing_newline() {
2144        let buf = Buffer::from_text("hello\nworld");
2145        let pos = buf.offset_up(Position::new(1, 5));
2146        assert_eq!(pos, Position::new(0, 5));
2147    }
2148
2149    #[test]
2150    fn test_offset_up_clamps_to_shorter_line() {
2151        let buf = Buffer::from_text("hi\nworld");
2152        let pos = buf.offset_up(Position::new(1, 5));
2153        assert_eq!(pos, Position::new(0, 2));
2154    }
2155
2156    #[test]
2157    fn test_delete_forward_multibyte() {
2158        let mut buf = Buffer::from_text("αβγ");
2159        buf.set_cursor(Position::new(0, 1));
2160        buf.delete_forward();
2161        assert_eq!(buf.text(), "αγ");
2162    }
2163
2164    #[test]
2165    fn test_delete_forward_newline() {
2166        let mut buf = Buffer::from_text("abc\ndef");
2167        buf.set_cursor(Position::new(0, 3));
2168        buf.delete_forward();
2169        assert_eq!(buf.text(), "abcdef");
2170    }
2171
2172    #[test]
2173    fn test_pos_to_char_and_char_to_pos() {
2174        let buf = Buffer::from_text("hello\nworld");
2175        assert_eq!(buf.pos_to_char(Position::new(0, 3)), 3);
2176        // "hello\n" = 6 chars, so line 1 col 2 = char 8
2177        assert_eq!(buf.pos_to_char(Position::new(1, 2)), 8);
2178        assert_eq!(buf.char_to_pos(3), Position::new(0, 3));
2179        assert_eq!(buf.char_to_pos(8), Position::new(1, 2));
2180    }
2181
2182    #[test]
2183    fn test_set_selection_normalizes_positions() {
2184        let mut buf = Buffer::from_text("hi\nworld");
2185        buf.set_selection(Some(Selection::new(
2186            Position::new(0, 100), // "hi" has only 2 chars
2187            Position::new(1, 3),
2188        )));
2189        let sel = buf.selection().unwrap();
2190        assert_eq!(sel.anchor, Position::new(0, 2));
2191        assert_eq!(sel.active, Position::new(1, 3));
2192    }
2193
2194    #[test]
2195    fn test_newline_undo_roundtrip() {
2196        let mut buf = Buffer::from_text("helloworld");
2197        buf.set_cursor(Position::new(0, 5));
2198        buf.insert_newline();
2199        assert_eq!(buf.text(), "hello\nworld");
2200        assert_eq!(buf.cursor(), Position::new(1, 0));
2201        buf.undo();
2202        assert_eq!(buf.text(), "helloworld");
2203        assert_eq!(buf.cursor(), Position::new(0, 5));
2204    }
2205
2206    // ── insert_at ──────────────────────────────────────────────────────────
2207
2208    #[test]
2209    fn test_insert_at_advances_cursor_when_at_position() {
2210        let mut buf = Buffer::from_text("hello");
2211        buf.set_cursor(Position::new(0, 5));
2212        buf.insert_at(Position::new(0, 5), " world");
2213        assert_eq!(buf.text(), "hello world");
2214        assert_eq!(buf.cursor(), Position::new(0, 11));
2215    }
2216
2217    #[test]
2218    fn test_insert_at_does_not_move_cursor_when_before_cursor() {
2219        let mut buf = Buffer::from_text("world");
2220        buf.set_cursor(Position::new(0, 5));
2221        buf.insert_at(Position::new(0, 0), "hello ");
2222        assert_eq!(buf.text(), "hello world");
2223        assert_eq!(buf.cursor(), Position::new(0, 5));
2224    }
2225
2226    // ── selected_text ──────────────────────────────────────────────────────
2227
2228    #[test]
2229    fn test_selected_text_single_line() {
2230        let mut buf = Buffer::from_text("hello world");
2231        buf.set_selection(Some(Selection {
2232            anchor: Position::new(0, 0),
2233            active: Position::new(0, 5),
2234        }));
2235        assert_eq!(buf.selected_text(), "hello");
2236    }
2237
2238    #[test]
2239    fn test_selected_text_multiline() {
2240        let mut buf = Buffer::from_text("hello\nworld");
2241        buf.set_selection(Some(Selection {
2242            anchor: Position::new(0, 3),
2243            active: Position::new(1, 3),
2244        }));
2245        assert_eq!(buf.selected_text(), "lo\nwor");
2246    }
2247
2248    #[test]
2249    fn test_selected_text_empty_when_no_selection() {
2250        let buf = Buffer::from_text("hello");
2251        assert_eq!(buf.selected_text(), "");
2252    }
2253
2254    // ── word_range_at ─────────────────────────────────────────────────────
2255
2256    #[test]
2257    fn test_word_range_at_middle_of_word() {
2258        let buf = Buffer::from_text("hello world");
2259        let (start, end) = buf.word_range_at(Position::new(0, 2));
2260        // Position 2 is in the middle of "hello", so start is 2, end is 5
2261        assert_eq!(start, 2);
2262        assert_eq!(end, 5);
2263    }
2264
2265    #[test]
2266    fn test_word_range_at_on_space() {
2267        let buf = Buffer::from_text("hello world");
2268        // Space is not a word character — just ensure no panic
2269        let _ = buf.word_range_at(Position::new(0, 5));
2270    }
2271
2272    // ── delete_range edge cases ───────────────────────────────────────────
2273
2274    #[test]
2275    fn test_delete_range_same_position_is_noop() {
2276        let mut buf = Buffer::from_text("hello");
2277        let pos = Position::new(0, 2);
2278        buf.delete_range(pos, pos);
2279        assert_eq!(buf.text(), "hello");
2280    }
2281
2282    // ── char_count, current_line, slice ──────────────────────────────────
2283
2284    #[test]
2285    fn test_char_count() {
2286        let buf = Buffer::from_text("hello\nworld");
2287        assert_eq!(buf.char_count(), 11);
2288    }
2289
2290    #[test]
2291    fn test_current_line() {
2292        let mut buf = Buffer::from_text("hello\nworld");
2293        buf.set_cursor(Position::new(1, 0));
2294        assert_eq!(buf.current_line(), 1);
2295    }
2296
2297    #[test]
2298    fn test_slice() {
2299        let buf = Buffer::from_text("hello world");
2300        let s = buf.slice(Position::new(0, 0)..Position::new(0, 5));
2301        assert_eq!(s, "hello");
2302    }
2303
2304    // ── history_len ──────────────────────────────────────────────────────
2305
2306    #[test]
2307    fn test_history_len() {
2308        let mut buf = Buffer::from_text("");
2309        assert_eq!(buf.history_len(), 0);
2310        buf.insert("a");
2311        assert!(buf.history_len() > 0);
2312    }
2313
2314    // ── scroll_to_cursor ──────────────────────────────────────────────────
2315
2316    #[test]
2317    fn test_scroll_to_cursor_scrolls_down_when_cursor_below_view() {
2318        let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
2319        buf.set_cursor(Position::new(4, 0));
2320        buf.scroll_to_cursor(3);
2321        assert!(buf.scroll >= 2);
2322    }
2323
2324    #[test]
2325    fn test_scroll_to_cursor_scrolls_up_when_cursor_above_view() {
2326        let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
2327        buf.scroll = 4;
2328        buf.set_cursor(Position::new(0, 0));
2329        buf.scroll_to_cursor(3);
2330        assert_eq!(buf.scroll, 0);
2331    }
2332
2333    // ── offset_down / offset_down_n ──────────────────────────────────────
2334
2335    #[test]
2336    fn test_offset_down_clamps_column_to_shorter_line() {
2337        let buf = Buffer::from_text("hello world\nhi");
2338        let pos = buf.offset_down(Position::new(0, 8));
2339        assert_eq!(pos, Position::new(1, 2));
2340    }
2341
2342    #[test]
2343    fn test_offset_down_at_last_line_stays() {
2344        let buf = Buffer::from_text("hello");
2345        let pos = buf.offset_down(Position::new(0, 3));
2346        assert_eq!(pos, Position::new(0, 3));
2347    }
2348
2349    #[test]
2350    fn test_offset_down_n_stops_at_last_line() {
2351        let buf = Buffer::from_text("a\nb\nc");
2352        let pos = buf.offset_down_n(Position::new(0, 0), 100);
2353        assert_eq!(pos.line, 2);
2354    }
2355
2356    // ── offset_left / offset_right wrapping ──────────────────────────────
2357
2358    #[test]
2359    fn test_offset_left_wraps_to_end_of_previous_line() {
2360        let buf = Buffer::from_text("hello\nworld");
2361        let pos = buf.offset_left(Position::new(1, 0));
2362        assert_eq!(pos, Position::new(0, 5));
2363    }
2364
2365    #[test]
2366    fn test_offset_right_wraps_to_start_of_next_line() {
2367        let buf = Buffer::from_text("hello\nworld");
2368        let pos = buf.offset_right(Position::new(0, 5));
2369        assert_eq!(pos, Position::new(1, 0));
2370    }
2371
2372    // ── insert_text_raw CRLF normalization ───────────────────────────────
2373
2374    #[test]
2375    fn test_insert_text_raw_normalizes_crlf() {
2376        let mut buf = Buffer::from_text("");
2377        buf.insert_text_raw("hello\r\nworld");
2378        assert_eq!(buf.text(), "hello\nworld");
2379    }
2380
2381    #[test]
2382    fn test_insert_text_raw_normalizes_cr_only() {
2383        let mut buf = Buffer::from_text("");
2384        buf.insert_text_raw("hello\rworld");
2385        assert_eq!(buf.text(), "hello\nworld");
2386    }
2387
2388    // ── replace_range no-op branch ─────────────────────────────────────────
2389
2390    #[test]
2391    fn test_replace_range_same_text_at_cursor_is_noop_for_history() {
2392        let mut buf = Buffer::from_text("hello");
2393        buf.set_cursor(Position::new(0, 0));
2394        let before_history_pos = buf.history_position();
2395        buf.replace_range(Position::new(0, 0), Position::new(0, 5), "hello");
2396        assert_eq!(buf.history_position(), before_history_pos);
2397        assert_eq!(buf.text(), "hello");
2398    }
2399
2400    // ── delete_word_forward starting on whitespace ────────────────────────
2401
2402    #[test]
2403    fn test_delete_word_forward_from_whitespace() {
2404        let mut buf = Buffer::from_text("hello   world");
2405        buf.set_cursor(Position::new(0, 5));
2406        buf.delete_word_forward();
2407        assert_eq!(buf.text(), "helloworld");
2408    }
2409
2410    // ── delete_line on last line ───────────────────────────────────────────
2411
2412    #[test]
2413    fn test_delete_line_last_line_leaves_empty() {
2414        let mut buf = Buffer::from_text("hello");
2415        buf.set_cursor(Position::new(0, 0));
2416        buf.delete_line();
2417        assert_eq!(buf.line_count(), 1);
2418        assert_eq!(buf.line(0).unwrap(), "");
2419    }
2420}