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, lines.clone());
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        let mut buf = 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        // Clamp cursor and selection to the actual content. Necessary when a
294        // non-dirty file is restored from saved state but the on-disk content
295        // has changed (fewer lines) since the state was last written.
296        let sel = buf.selection.take();
297        if let Some(s) = sel {
298            buf.set_selection(Some(Selection {
299                anchor: buf.normalize_position(s.anchor),
300                active: buf.normalize_position(s.active),
301            }));
302        } else {
303            buf.normalize_cursor();
304        }
305        buf
306    }
307
308    pub fn id(&self) -> BufferId {
309        self.id
310    }
311
312    pub fn version(&self) -> Version {
313        self.version
314    }
315
316    pub fn snapshot(&self) -> BufferSnapshot {
317        BufferSnapshot {
318            text: self.text.clone(),
319            version: self.version,
320        }
321    }
322
323    fn line_type(&self) -> LineType {
324        LineType::LF
325    }
326
327    fn pos_to_byte(&self, pos: Position) -> usize {
328        let line_count = self.text.len_lines(self.line_type());
329        if pos.line >= line_count {
330            if pos.line == line_count && pos.column == 0 && line_count > 0 {
331                let last_line_idx = line_count - 1;
332                let last_line = self.text.line(last_line_idx, self.line_type());
333                if last_line.chars().last() == Some('\n') {
334                    let last_line_start =
335                        self.text.line_to_byte_idx(last_line_idx, self.line_type());
336                    return last_line_start + last_line.len() - 1;
337                }
338            }
339            return self.text.len();
340        }
341        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
342        let line = self.text.line(pos.line, self.line_type());
343
344        let mut char_count = 0;
345        let mut byte_offset = 0;
346        for ch in line.chars() {
347            if char_count >= pos.column {
348                break;
349            }
350            byte_offset += ch.len_utf8();
351            char_count += 1;
352        }
353        if pos.column > char_count {
354            byte_offset = line.len();
355        }
356        line_start_byte + byte_offset
357    }
358
359    fn byte_to_pos(&self, byte_idx: usize) -> Position {
360        let clamped_byte_idx = byte_idx.min(self.text.len());
361        let line_count = self.text.len_lines(self.line_type());
362
363        if clamped_byte_idx == self.text.len() {
364            if line_count == 0 {
365                return Position::new(0, 0);
366            }
367            let last_line_idx = line_count - 1;
368            let last_line = self.text.line(last_line_idx, self.line_type());
369            let last_line_len = last_line.len_chars();
370            let has_trailing_nl = last_line.chars().last() == Some('\n');
371            if has_trailing_nl {
372                return Position::new(last_line_idx + 1, 0);
373            }
374            return Position::new(last_line_idx, last_line_len);
375        }
376
377        let byte_idx = clamped_byte_idx;
378        let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
379        let line_start_byte = self.text.line_to_byte_idx(line_idx, self.line_type());
380        let char_idx = self.text.byte_to_char_idx(byte_idx);
381        let line_start_char = self.text.byte_to_char_idx(line_start_byte);
382        let line = self.text.line(line_idx, self.line_type());
383        let line_len_chars = line.len_chars();
384        let line_has_trailing_nl = line.chars().last() == Some('\n');
385
386        let column = char_idx - line_start_char;
387        if column >= line_len_chars && line_has_trailing_nl {
388            return Position::new(line_idx + 1, 0);
389        }
390        Position::new(line_idx, column)
391    }
392
393    fn cursor_to_byte(&self) -> usize {
394        self.pos_to_byte(self.cursor)
395    }
396
397    /// Convert a `Position` to a global char index using ropey's char metric.
398    pub fn pos_to_char(&self, pos: Position) -> usize {
399        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
400        let line_start_char = self.text.byte_to_char_idx(line_start_byte);
401        line_start_char + pos.column
402    }
403
404    /// Convert a global char index to a `Position`.
405    pub fn char_to_pos(&self, char_idx: usize) -> Position {
406        let byte_idx = self.text.char_to_byte_idx(char_idx).min(self.text.len());
407        self.byte_to_pos(byte_idx)
408    }
409
410    pub fn cursor(&self) -> Position {
411        self.cursor
412    }
413
414    pub fn selection(&self) -> Option<Selection> {
415        self.selection
416    }
417
418    pub fn set_cursor(&mut self, pos: Position) {
419        self.selection = None;
420        self.cursor = self.normalize_position(pos);
421    }
422
423    pub fn set_selection(&mut self, selection: Option<Selection>) {
424        self.selection = selection.map(|sel| Selection {
425            anchor: self.normalize_position(sel.anchor),
426            active: self.normalize_position(sel.active),
427        });
428        if let Some(ref sel) = self.selection {
429            self.cursor = sel.active;
430        }
431    }
432
433    /// Clamp `pos` to a valid (line, column) within the current text.
434    pub fn normalize_position(&self, pos: Position) -> Position {
435        let line_count = self.text.len_lines(self.line_type());
436        if line_count == 0 {
437            return Position::new(0, 0);
438        }
439        let line = pos.line.min(line_count - 1);
440        let line_len = self.line_display_len(line);
441        Position::new(line, pos.column.min(line_len))
442    }
443
444    pub fn normalize_cursor(&mut self) {
445        self.cursor = self.normalize_position(self.cursor);
446    }
447
448    /// Return the usable character length of a line, excluding the trailing newline.
449    fn line_display_len(&self, line_idx: usize) -> usize {
450        let line = self.text.line(line_idx, self.line_type());
451        let has_trailing_nl = line.chars().last() == Some('\n');
452        line.len_chars().saturating_sub(has_trailing_nl as usize)
453    }
454
455    pub fn line_count(&self) -> usize {
456        self.text.len_lines(self.line_type()).max(1)
457    }
458
459    pub fn current_line(&self) -> usize {
460        self.cursor.line
461    }
462
463    pub fn char_count(&self) -> usize {
464        self.text.len_chars()
465    }
466
467    pub fn line(&self, line_idx: usize) -> Option<String> {
468        let actual_line_count = self.text.len_lines(self.line_type());
469        if line_idx >= actual_line_count {
470            return None;
471        }
472        let mut line_text = self.text.line(line_idx, self.line_type()).to_string();
473        if line_text.ends_with('\n') {
474            line_text.pop();
475        }
476        if line_text.ends_with('\r') {
477            line_text.pop();
478        }
479        Some(line_text)
480    }
481
482    pub fn lines(&self) -> Vec<String> {
483        (0..self.text.len_lines(self.line_type()))
484            .map(|i| {
485                let mut s: String = self.text.line(i, self.line_type()).into();
486                if s.ends_with('\n') {
487                    s.pop();
488                }
489                if s.ends_with('\r') {
490                    s.pop();
491                }
492                s
493            })
494            .collect()
495    }
496
497    /// Return a shared `Arc<Vec<String>>` for the current buffer contents.
498    /// The result is cached and invalidated on mutations to avoid rebuilding
499    /// the Vec for every highlight request.
500    pub fn lines_arc(&mut self) -> Arc<Vec<String>> {
501        if let Some(ref cached) = self.cached_lines {
502            return Arc::clone(cached);
503        }
504        let v = self.lines();
505        let arc = Arc::new(v);
506        self.cached_lines = Some(arc.clone());
507        arc
508    }
509
510    /// Consume the buffer and return its lines, preferring a cached Arc<Vec<String>>
511    /// to avoid cloning when possible. This is useful when stashing buffers during
512    /// screen switches where the buffer can be moved without extra allocations.
513    pub fn into_lines(self) -> Vec<String> {
514        if let Some(arc) = self.cached_lines {
515            match Arc::try_unwrap(arc) {
516                Ok(v) => v,
517                Err(arc) => (*arc).clone(),
518            }
519        } else {
520            self.lines()
521        }
522    }
523
524    pub fn is_empty(&self) -> bool {
525        self.text.len_chars() == 0
526    }
527
528    pub fn is_dirty(&self) -> bool {
529        self.dirty
530    }
531
532    fn increment_version(&mut self) {
533        self.version.increment();
534        self.dirty = true;
535        self.file_hash = None;
536        self.file_mtime = None;
537        self.file_size = None;
538        self.cached_lines = None;
539    }
540
541    pub fn can_undo(&self) -> bool {
542        self.history.can_undo()
543    }
544
545    pub fn can_redo(&self) -> bool {
546        self.history.can_redo()
547    }
548
549    pub fn history_len(&self) -> usize {
550        self.history.len()
551    }
552
553    pub fn history_position(&self) -> usize {
554        self.history.current_position()
555    }
556
557    pub fn apply_change(&mut self, change: ChangeSet, kind: ChangeKind) {
558        if change.is_empty() {
559            return;
560        }
561
562        // Inside a transaction: buffer the raw ops and return; history is
563        // not touched until end_transaction() commits everything at once.
564        if let Some(tx) = &mut self.active_tx {
565            tx.ops.extend(change.ops);
566            return;
567        }
568
569        // Coalesce adjacent typing/deletion into a single undo step when
570        // the edits are contiguous, within the merge window, and don't
571        // cross line boundaries.  All other kinds (Replace, Structural, …)
572        // always get their own entry so they can be undone atomically.
573        let merged = matches!(kind, ChangeKind::InsertText | ChangeKind::DeleteText)
574            && self.history.try_merge(change.clone(), kind, self.version);
575
576        if !merged {
577            self.history.push(change, kind, self.version);
578        }
579    }
580
581    /// Begin an explicit transaction.  All subsequent calls to
582    /// [`Buffer::apply_change`] will buffer their ops instead of writing to history.
583    /// Call [`Buffer::end_transaction`] to commit everything as one undo step.
584    ///
585    /// If a transaction is already active this is a no-op (the outermost
586    /// transaction wins).  In debug builds a panic is emitted to surface
587    /// accidental nesting early.
588    pub fn begin_transaction(&mut self, kind: ChangeKind) {
589        debug_assert!(
590            self.active_tx.is_none(),
591            "begin_transaction called while a transaction is already active; \
592             nested transactions are not supported"
593        );
594        if self.active_tx.is_some() {
595            return;
596        }
597        self.active_tx = Some(Transaction {
598            ops: Vec::new(),
599            kind,
600            before_cursor: self.cursor,
601            before_selection: self.selection,
602        });
603    }
604
605    /// Commit the current transaction to history as a **single undo step**.
606    ///
607    /// * The committed entry always gets its own history slot — `try_merge`
608    ///   is bypassed, so the transaction is never accidentally coalesced with
609    ///   surrounding typing.
610    /// * `history.push` truncates any future (redo) entries, so a
611    ///   transaction after undo correctly discards the forward branch.
612    /// * `increment_version` is called on commit so the buffer version
613    ///   advances once for the whole group (consistent with LSP sync).
614    ///
615    /// If no transaction is active, or if no ops were recorded, this is a
616    /// no-op.
617    pub fn end_transaction(&mut self) {
618        let tx = match self.active_tx.take() {
619            Some(tx) => tx,
620            None => return,
621        };
622        if tx.ops.is_empty() {
623            return;
624        }
625        let change = ChangeSet::new(
626            tx.ops,
627            tx.before_cursor,
628            self.cursor,
629            tx.before_selection,
630            self.selection,
631        );
632        // Always push; the transaction *is* the merge unit.
633        // push() also truncates the redo stack, so branching history is safe.
634        self.history.push(change, tx.kind, self.version);
635        // Advance the version once for the committed group so that external
636        // consumers (LSP, syntax highlighter) see exactly one change event.
637        self.increment_version();
638    }
639
640    /// Discard the current transaction **without** recording a history entry.
641    ///
642    /// ⚠️  Text mutations that occurred within the transaction are **not**
643    /// rolled back — only the pending history entry is dropped.  The buffer
644    /// will be in a modified-but-untracked state after this call.  Use this
645    /// only when you intend to handle the rollback yourself (e.g. by
646    /// reloading from disk), or when you are certain no edits were made.
647    pub fn abort_transaction(&mut self) {
648        self.active_tx = None;
649    }
650
651    /// Returns `true` if a transaction is currently open.
652    pub fn in_transaction(&self) -> bool {
653        self.active_tx.is_some()
654    }
655
656    pub fn insert(&mut self, text: &str) {
657        // If there's a selection, replace it atomically as a single undo step.
658        if let Some(sel) = self.selection {
659            let start = sel.min();
660            let end = sel.max();
661            let before_cursor = self.cursor;
662            let before_selection = self.selection;
663
664            let start_byte = self.pos_to_byte(start);
665            let end_byte = self.pos_to_byte(end).min(self.text.len());
666            let old_text: String = self.text.slice(start_byte..end_byte).into();
667
668            self.text.remove(start_byte..end_byte);
669            self.text.insert(start_byte, text);
670
671            let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
672            let after_cursor = self.byte_to_pos(
673                self.text
674                    .char_to_byte_idx(new_char_idx)
675                    .min(self.text.len()),
676            );
677            self.cursor = after_cursor;
678            self.selection = None;
679
680            let op = BufferOp::Replace {
681                range: start..end,
682                text: text.to_string(),
683                old_text,
684                end_position: after_cursor,
685            };
686            let change = ChangeSet::new(
687                vec![op],
688                before_cursor,
689                after_cursor,
690                before_selection,
691                None,
692            );
693            self.apply_change(change, ChangeKind::Replace);
694            self.increment_version();
695            return;
696        }
697
698        let before_cursor = self.cursor;
699        let before_selection = self.selection;
700
701        let byte_idx = self.cursor_to_byte();
702        let old_char_idx = self.text.byte_to_char_idx(byte_idx);
703        self.text.insert(byte_idx, text);
704
705        let new_char_idx = old_char_idx + text.chars().count();
706        let new_char_byte = self
707            .text
708            .char_to_byte_idx(new_char_idx)
709            .min(self.text.len());
710        let new_cursor = self.byte_to_pos(new_char_byte);
711        self.cursor = new_cursor;
712
713        let after_cursor = new_cursor;
714        let after_selection = None;
715
716        let range = before_cursor..after_cursor;
717        let op = BufferOp::Replace {
718            range,
719            text: text.to_string(),
720            old_text: String::new(),
721            end_position: after_cursor,
722        };
723
724        let change = ChangeSet::new(
725            vec![op],
726            before_cursor,
727            after_cursor,
728            before_selection,
729            after_selection,
730        );
731
732        self.apply_change(change, ChangeKind::InsertText);
733        self.increment_version();
734    }
735
736    /// Delete the current selection if one exists. Returns true if a selection was deleted.
737    fn delete_selection_if_active(&mut self) -> bool {
738        if let Some(sel) = self.selection {
739            self.delete_range(sel.min(), sel.max());
740            true
741        } else {
742            false
743        }
744    }
745
746    pub fn delete_backward(&mut self) {
747        if self.delete_selection_if_active() {
748            return;
749        }
750
751        let byte_idx = self.cursor_to_byte();
752        if byte_idx == 0 {
753            return;
754        }
755
756        let before_cursor = self.cursor;
757        let before_selection = self.selection;
758
759        let char_idx = self.text.byte_to_char_idx(byte_idx);
760        if char_idx == 0 {
761            return;
762        }
763        let prev_char_idx = char_idx - 1;
764        let del_start_byte = self.text.char_to_byte_idx(prev_char_idx);
765        let del_end_byte = byte_idx;
766        let prev_char_byte_idx = del_start_byte;
767        let prev_char = self.text.char(prev_char_byte_idx);
768
769        let new_cursor_pos: Position;
770        if prev_char == '\n' {
771            let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
772            if line_idx > 0 {
773                let new_cursor_col = self.line_display_len(line_idx - 1);
774                new_cursor_pos = Position::new(line_idx - 1, new_cursor_col);
775            } else {
776                new_cursor_pos = before_cursor;
777            }
778        } else {
779            let new_col = before_cursor.column.saturating_sub(1);
780            new_cursor_pos = Position::new(before_cursor.line, new_col);
781        }
782
783        let deleted_text: String = self.text.slice(del_start_byte..del_end_byte).into();
784
785        self.text.remove(del_start_byte..del_end_byte);
786
787        self.cursor = new_cursor_pos;
788        self.selection = None;
789
790        let range = new_cursor_pos..before_cursor;
791        let op = BufferOp::Replace {
792            range,
793            text: String::new(),
794            old_text: deleted_text,
795            end_position: new_cursor_pos,
796        };
797
798        let change = ChangeSet::new(
799            vec![op],
800            before_cursor,
801            new_cursor_pos,
802            before_selection,
803            None,
804        );
805
806        self.apply_change(change, ChangeKind::DeleteText);
807        self.increment_version();
808    }
809
810    pub fn delete_forward(&mut self) {
811        if self.delete_selection_if_active() {
812            return;
813        }
814
815        let byte_idx = self.cursor_to_byte();
816        if byte_idx >= self.text.len() {
817            return;
818        }
819
820        let before_cursor = self.cursor;
821        let before_selection = self.selection;
822
823        // Advance by exactly one char using char indices (correct for multi-byte UTF-8).
824        let char_idx = self.text.byte_to_char_idx(byte_idx);
825        let next_byte = self
826            .text
827            .char_to_byte_idx(char_idx + 1)
828            .min(self.text.len());
829
830        let deleted_text: String = self.text.slice(byte_idx..next_byte).into();
831        self.text.remove(byte_idx..next_byte);
832
833        let after_cursor = before_cursor;
834        let after_selection = None;
835
836        let range = before_cursor..after_cursor;
837        let op = BufferOp::Replace {
838            range,
839            text: String::new(),
840            old_text: deleted_text,
841            end_position: before_cursor,
842        };
843
844        let change = ChangeSet::new(
845            vec![op],
846            before_cursor,
847            after_cursor,
848            before_selection,
849            after_selection,
850        );
851
852        self.apply_change(change, ChangeKind::DeleteText);
853        self.increment_version();
854    }
855
856    pub fn delete_range(&mut self, start_pos: Position, end_pos: Position) {
857        if start_pos == end_pos {
858            return;
859        }
860
861        let start_byte = self.pos_to_byte(start_pos);
862        let end_byte = self.pos_to_byte(end_pos).min(self.text.len());
863
864        if start_byte >= self.text.len() || start_byte >= end_byte {
865            return;
866        }
867
868        let before_cursor = self.cursor;
869        let before_selection = self.selection;
870
871        let deleted_text: String = self.text.slice(start_byte..end_byte).into();
872        self.text.remove(start_byte..end_byte);
873
874        self.cursor = start_pos;
875        self.selection = None;
876
877        let range = start_pos..end_pos;
878        let op = BufferOp::Replace {
879            range,
880            text: String::new(),
881            old_text: deleted_text,
882            end_position: start_pos,
883        };
884
885        let change = ChangeSet::new(vec![op], before_cursor, start_pos, before_selection, None);
886
887        self.apply_change(change, ChangeKind::DeleteText);
888        self.increment_version();
889    }
890
891    pub fn replace_range(&mut self, start: Position, end: Position, text: &str) {
892        let start_byte = self.pos_to_byte(start);
893        let end_byte = self.pos_to_byte(end).min(self.text.len());
894
895        if start_byte >= self.text.len() && start != end {
896            return;
897        }
898
899        let before_cursor = self.cursor;
900        let before_selection = self.selection;
901
902        let actual_end_byte = end_byte.min(self.text.len());
903        let old_text: String = self.text.slice(start_byte..actual_end_byte).into();
904
905        if old_text == text && before_cursor == start {
906            return;
907        }
908
909        self.text.remove(start_byte..actual_end_byte);
910        self.text.insert(start_byte, text);
911
912        let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
913        let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
914        self.cursor = after_cursor;
915        self.selection = None;
916
917        let range = start..end;
918        let op = BufferOp::Replace {
919            range,
920            text: text.to_string(),
921            old_text,
922            end_position: after_cursor,
923        };
924
925        let change = ChangeSet::new(
926            vec![op],
927            before_cursor,
928            after_cursor,
929            before_selection,
930            None,
931        );
932
933        self.apply_change(change, ChangeKind::Replace);
934        self.increment_version();
935    }
936
937    pub fn insert_newline(&mut self) {
938        let before_cursor = self.cursor;
939        let before_selection = self.selection;
940
941        let byte_idx = self.cursor_to_byte();
942        self.text.insert(byte_idx, "\n");
943
944        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + 1;
945        let new_cursor_byte = self
946            .text
947            .char_to_byte_idx(new_char_idx)
948            .min(self.text.len());
949        let new_cursor = self.byte_to_pos(new_cursor_byte);
950        self.cursor = new_cursor;
951
952        let range = before_cursor..new_cursor;
953        let op = BufferOp::Replace {
954            range,
955            text: "\n".to_string(),
956            old_text: String::new(),
957            end_position: new_cursor,
958        };
959
960        let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
961
962        self.apply_change(change, ChangeKind::Structural);
963        self.increment_version();
964    }
965
966    pub fn insert_newline_with_indent(&mut self, _use_spaces: bool, _indent_width: usize) {
967        // Extract leading whitespace from the current line
968        // This ensures we carry forward the same indentation for the new line
969        let leading_indent = if self.cursor.line < self.text.len_lines(self.line_type()) {
970            let line = self.text.line(self.cursor.line, self.line_type());
971            line.chars()
972                .take_while(|c| *c == ' ' || *c == '\t')
973                .collect::<String>()
974        } else {
975            String::new()
976        };
977
978        // Always use the leading indentation from the current line (even if empty)
979        // This matches the user's existing indentation pattern
980        let text = format!("\n{}", leading_indent);
981
982        let before_cursor = self.cursor;
983        let before_selection = self.selection;
984
985        let byte_idx = self.cursor_to_byte();
986        self.text.insert(byte_idx, &text);
987
988        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
989        let new_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
990        self.cursor = new_cursor;
991
992        let range = before_cursor..new_cursor;
993        let op = BufferOp::Replace {
994            range,
995            text,
996            old_text: String::new(),
997            end_position: new_cursor,
998        };
999
1000        let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
1001
1002        self.apply_change(change, ChangeKind::Structural);
1003        self.increment_version();
1004    }
1005
1006    pub fn delete_word_backward(&mut self) {
1007        let end_byte = self.cursor_to_byte();
1008        if end_byte == 0 {
1009            return;
1010        }
1011
1012        let before_cursor = self.cursor;
1013        let before_selection = self.selection;
1014
1015        // Use the same token logic as `word_boundary_prev` so deletion mirrors movement.
1016        let start_pos = self.word_boundary_prev(self.cursor);
1017        let start_byte = self.pos_to_byte(start_pos);
1018
1019        if start_byte < end_byte {
1020            let deleted_text: String = self.text.slice(start_byte..end_byte).into();
1021            self.text.remove(start_byte..end_byte);
1022            let new_pos = self.byte_to_pos(start_byte);
1023            self.cursor = new_pos;
1024
1025            let range = new_pos..before_cursor;
1026            let op = BufferOp::Replace {
1027                range,
1028                text: String::new(),
1029                old_text: deleted_text,
1030                end_position: new_pos,
1031            };
1032
1033            let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
1034
1035            self.apply_change(change, ChangeKind::DeleteText);
1036            self.increment_version();
1037        }
1038    }
1039
1040    pub fn delete_word_forward(&mut self) {
1041        let start = self.cursor_to_byte();
1042        if start >= self.text.len() {
1043            return;
1044        }
1045
1046        let before_cursor = self.cursor;
1047        let before_selection = self.selection;
1048
1049        let mut char_idx = self.text.byte_to_char_idx(start);
1050        let end_char = self.text.len_chars();
1051
1052        let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
1053        let at_whitespace = first_char.is_whitespace();
1054
1055        while char_idx < end_char
1056            && self
1057                .text
1058                .char(self.text.char_to_byte_idx(char_idx))
1059                .is_whitespace()
1060        {
1061            char_idx += 1;
1062        }
1063
1064        if !at_whitespace {
1065            while char_idx < end_char {
1066                let curr_byte = self.text.char_to_byte_idx(char_idx);
1067                if self.text.char(curr_byte).is_whitespace() {
1068                    break;
1069                }
1070                char_idx += 1;
1071            }
1072
1073            while char_idx < end_char
1074                && self
1075                    .text
1076                    .char(self.text.char_to_byte_idx(char_idx))
1077                    .is_whitespace()
1078            {
1079                char_idx += 1;
1080            }
1081        }
1082
1083        let end_byte = if char_idx < end_char {
1084            self.text.char_to_byte_idx(char_idx)
1085        } else {
1086            self.text.len()
1087        };
1088        let start_byte = self
1089            .text
1090            .char_to_byte_idx(self.text.byte_to_char_idx(start));
1091
1092        if start_byte < end_byte {
1093            let deleted_text: String = self.text.slice(start_byte..end_byte).into();
1094            self.text.remove(start_byte..end_byte);
1095
1096            let range = before_cursor..before_cursor;
1097            let op = BufferOp::Replace {
1098                range,
1099                text: String::new(),
1100                old_text: deleted_text,
1101                end_position: before_cursor,
1102            };
1103
1104            let change = ChangeSet::new(
1105                vec![op],
1106                before_cursor,
1107                before_cursor,
1108                before_selection,
1109                None,
1110            );
1111
1112            self.apply_change(change, ChangeKind::DeleteText);
1113            self.increment_version();
1114        }
1115    }
1116
1117    pub fn word_range_at(&self, pos: Position) -> (usize, usize) {
1118        let byte_idx = self.pos_to_byte(pos);
1119        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1120        let line = self.text.line(pos.line, self.line_type());
1121
1122        let mut word_start = 0;
1123        let mut word_end = line.len_chars();
1124        let mut in_word = false;
1125
1126        for (i, (byte_offset, ch)) in line.char_indices().enumerate() {
1127            let abs_byte = line_start_byte + byte_offset;
1128            if abs_byte >= byte_idx && ch.is_alphanumeric() && !in_word {
1129                word_start = i;
1130                in_word = true;
1131            }
1132            if in_word && !ch.is_alphanumeric() {
1133                word_end = i;
1134                break;
1135            }
1136        }
1137
1138        (word_start, word_end)
1139    }
1140
1141    pub fn insert_at(&mut self, pos: Position, text: &str) {
1142        let before_cursor = self.cursor;
1143        let before_selection = self.selection;
1144
1145        let byte_idx = self.pos_to_byte(pos);
1146        self.text.insert(byte_idx, text);
1147
1148        let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
1149        let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
1150        if pos == before_cursor {
1151            self.cursor = after_cursor;
1152        }
1153
1154        let range = pos..after_cursor;
1155        let op = BufferOp::Replace {
1156            range,
1157            text: text.to_string(),
1158            old_text: String::new(),
1159            end_position: after_cursor,
1160        };
1161
1162        let change = ChangeSet::new(
1163            vec![op],
1164            before_cursor,
1165            after_cursor,
1166            before_selection,
1167            None,
1168        );
1169
1170        self.apply_change(change, ChangeKind::InsertText);
1171        self.increment_version();
1172    }
1173
1174    pub fn insert_text_raw(&mut self, text: &str) {
1175        let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1176        self.insert(&normalized);
1177    }
1178
1179    pub fn delete_line(&mut self) -> String {
1180        let line_idx = self.cursor.line;
1181        if line_idx >= self.text.len_lines(self.line_type()) {
1182            return String::new();
1183        }
1184
1185        let line_start = self.text.line_to_byte_idx(line_idx, self.line_type());
1186        let line_end = if line_idx + 1 < self.text.len_lines(self.line_type()) {
1187            self.text.line_to_byte_idx(line_idx + 1, self.line_type())
1188        } else {
1189            self.text.len()
1190        };
1191
1192        let deleted: String = self.text.slice(line_start..line_end).into();
1193
1194        let before_cursor = self.cursor;
1195        let before_selection = self.selection;
1196
1197        self.text.remove(line_start..line_end);
1198
1199        if self.text.len_lines(self.line_type()) == 0 {
1200            self.text.insert(0, "");
1201        }
1202
1203        let new_line_idx = line_idx.min(self.text.len_lines(self.line_type()).saturating_sub(1));
1204        let new_pos = Position::new(new_line_idx, 0);
1205        self.cursor = new_pos;
1206        self.normalize_cursor();
1207
1208        let range = new_pos..before_cursor;
1209        let op = BufferOp::Replace {
1210            range,
1211            text: String::new(),
1212            old_text: deleted.clone(),
1213            end_position: new_pos,
1214        };
1215
1216        let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
1217
1218        self.apply_change(change, ChangeKind::DeleteText);
1219        self.increment_version();
1220
1221        deleted
1222    }
1223
1224    pub fn selected_text_all(&self) -> String {
1225        self.selected_text()
1226    }
1227
1228    pub fn cursor_left(&mut self) -> Position {
1229        let pos = self.offset_left(self.cursor);
1230        self.cursor = pos;
1231        self.cursor
1232    }
1233
1234    pub fn offset_left(&self, pos: Position) -> Position {
1235        if pos.column > 0 {
1236            Position::new(pos.line, pos.column - 1)
1237        } else if pos.line > 0 {
1238            let prev_line = pos.line - 1;
1239            let prev_line_len = self.line_display_len(prev_line);
1240            Position::new(prev_line, prev_line_len)
1241        } else {
1242            pos
1243        }
1244    }
1245
1246    pub fn cursor_right(&mut self) -> Position {
1247        let pos = self.offset_right(self.cursor);
1248        self.cursor = pos;
1249        self.cursor
1250    }
1251
1252    pub fn offset_right(&self, pos: Position) -> Position {
1253        let line_len = self.line_display_len(pos.line);
1254        if pos.column < line_len {
1255            Position::new(pos.line, pos.column + 1)
1256        } else if pos.line < self.text.len_lines(self.line_type()) - 1 {
1257            Position::new(pos.line + 1, 0)
1258        } else {
1259            pos
1260        }
1261    }
1262
1263    /// Apply a cursor movement, updating self.cursor to the new position.
1264    fn apply_movement(&mut self, new_pos: Position) -> Position {
1265        self.cursor = new_pos;
1266        self.cursor
1267    }
1268
1269    pub fn cursor_up(&mut self) -> Position {
1270        self.apply_movement(self.offset_up(self.cursor))
1271    }
1272
1273    pub fn offset_up(&self, pos: Position) -> Position {
1274        if pos.line == 0 { pos } else { self.offset_up_n(pos, 1) }
1275    }
1276
1277    pub fn cursor_down(&mut self) -> Position {
1278        self.apply_movement(self.offset_down(self.cursor))
1279    }
1280
1281    pub fn offset_down(&self, pos: Position) -> Position {
1282        if pos.line >= self.text.len_lines(self.line_type()).saturating_sub(1) {
1283            pos
1284        } else {
1285            self.offset_down_n(pos, 1)
1286        }
1287    }
1288
1289    pub fn cursor_up_n(&mut self, n: usize) -> Position {
1290        self.apply_movement(self.offset_up_n(self.cursor, n))
1291    }
1292
1293    pub fn offset_up_n(&self, pos: Position, n: usize) -> Position {
1294        let new_line = pos.line.saturating_sub(n);
1295        let line_len = self.line_display_len(new_line);
1296        Position::new(new_line, pos.column.min(line_len))
1297    }
1298
1299    pub fn cursor_down_n(&mut self, n: usize) -> Position {
1300        self.apply_movement(self.offset_down_n(self.cursor, n))
1301    }
1302
1303    pub fn offset_down_n(&self, pos: Position, n: usize) -> Position {
1304        let new_line = (pos.line + n).min(self.text.len_lines(self.line_type()).saturating_sub(1));
1305        let line_len = self.line_display_len(new_line);
1306        Position::new(new_line, pos.column.min(line_len))
1307    }
1308
1309    pub fn cursor_page_up(&mut self, height: usize) -> Position {
1310        self.apply_movement(self.offset_up_n(self.cursor, height))
1311    }
1312
1313    pub fn cursor_page_down(&mut self, height: usize) -> Position {
1314        self.apply_movement(self.offset_down_n(self.cursor, height))
1315    }
1316
1317    pub fn cursor_line_start(&mut self) -> Position {
1318        self.cursor.column = 0;
1319        self.cursor
1320    }
1321
1322    pub fn offset_line_start(&self, pos: Position) -> Position {
1323        Position::new(pos.line, 0)
1324    }
1325
1326    pub fn cursor_line_end(&mut self) -> Position {
1327        self.cursor.column = self.line_display_len(self.cursor.line);
1328        self.cursor
1329    }
1330
1331    pub fn offset_line_end(&self, pos: Position) -> Position {
1332        Position::new(pos.line, self.line_display_len(pos.line))
1333    }
1334
1335    pub fn cursor_word_prev(&mut self) -> Position {
1336        self.apply_movement(self.word_boundary_prev(self.cursor))
1337    }
1338
1339    pub fn cursor_word_next(&mut self) -> Position {
1340        self.apply_movement(self.word_boundary_next_for_selection(self.cursor))
1341    }
1342
1343    pub fn word_boundary_prev(&self, pos: Position) -> Position {
1344        // If at logical start of line (column 0 or first non-whitespace), move to end of previous line (do not attempt token logic).
1345        if pos.line > 0 {
1346            let line_slice = self.text.line(pos.line, self.line_type());
1347            let mut first_non_ws_col: Option<usize> = None;
1348            for (i, c) in line_slice.chars().enumerate() {
1349                if !c.is_whitespace() || c == '\n' {
1350                    first_non_ws_col = Some(i);
1351                    break;
1352                }
1353            }
1354            if pos.column == 0 || first_non_ws_col == Some(pos.column) {
1355                let prev_line = pos.line - 1;
1356                return Position::new(prev_line, self.line_display_len(prev_line));
1357            }
1358        }
1359
1360        let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1361        let byte_idx = line_start_byte + self.text.char_to_byte_idx(pos.column);
1362
1363        if byte_idx == 0 {
1364            return pos;
1365        }
1366
1367        // Start from the character immediately before the cursor.
1368        let mut char_idx = self.text.byte_to_char_idx(byte_idx);
1369        if char_idx == 0 {
1370            return pos;
1371        }
1372        char_idx = char_idx.saturating_sub(1);
1373
1374        // Phase 1: skip whitespace (but not newlines) immediately before the cursor.
1375        while char_idx > 0 {
1376            let c_byte = self.text.char_to_byte_idx(char_idx);
1377            let c = self.text.char(c_byte);
1378            if !c.is_whitespace() || c == '\n' {
1379                break;
1380            }
1381            char_idx = char_idx.saturating_sub(1);
1382        }
1383
1384        // Determine token type: punctuation vs other (word) characters.
1385        let c_byte = self.text.char_to_byte_idx(char_idx);
1386        let first_char = self.text.char(c_byte);
1387        let is_punct = first_char.is_ascii_punctuation();
1388
1389        // Phase 2: move left over the token of the same type.
1390        while char_idx > 0 {
1391            let prev_byte = self.text.char_to_byte_idx(char_idx.saturating_sub(1));
1392            let prev_char = self.text.char(prev_byte);
1393            if is_punct {
1394                if !prev_char.is_ascii_punctuation() {
1395                    break;
1396                }
1397            } else if prev_char.is_whitespace() || prev_char.is_ascii_punctuation() {
1398                break;
1399            }
1400            char_idx = char_idx.saturating_sub(1);
1401        }
1402
1403        self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
1404    }
1405
1406    pub fn word_boundary_next(&self, pos: Position) -> Position {
1407        self.word_boundary_next_impl(pos, true)
1408    }
1409
1410    pub fn word_boundary_next_for_selection(&self, pos: Position) -> Position {
1411        self.word_boundary_next_impl(pos, false)
1412    }
1413
1414    fn word_boundary_next_impl(&self, mut pos: Position, skip_whitespace_after: bool) -> Position {
1415        let end_char = self.text.len_chars();
1416
1417        // Precompute line-local char bounds so we can clamp to end-of-line and
1418        // avoid crossing to the next line.
1419        let mut line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1420        let mut line_start_char = self.text.byte_to_char_idx(line_start_byte);
1421        let mut line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
1422        let line_slice = self.text.line(pos.line, self.line_type());
1423        // Printable length excludes the trailing newline if present (rope lines include the newline
1424        // on all but the last line). Use printable_line_len for 'visual' end-of-line semantics.
1425        let mut printable_line_len = if line_slice.chars().last() == Some('\n') {
1426            line_len_chars.saturating_sub(1)
1427        } else {
1428            line_len_chars
1429        };
1430        let mut printable_line_end_char_idx = line_start_char + printable_line_len;
1431
1432        let mut char_idx = self.pos_to_char(pos);
1433
1434        // If at or beyond EOF, stay put.
1435        if char_idx >= end_char {
1436            return pos;
1437        }
1438
1439
1440        // If already at or beyond end-of-line (visual EOL), attempt to move to the first
1441        // token on the next line instead of clamping here.
1442        let total_lines = self.text.len_lines(self.line_type());
1443        if char_idx >= printable_line_end_char_idx {
1444            if pos.line + 1 >= total_lines {
1445                // No next line, clamp to current line printable end.
1446                return Position::new(pos.line, printable_line_len);
1447            }
1448
1449            // Move to the start of the next line and recompute bounds.
1450            pos = Position::new(pos.line + 1, 0);
1451            line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
1452            line_start_char = self.text.byte_to_char_idx(line_start_byte);
1453            line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
1454            let line_slice = self.text.line(pos.line, self.line_type());
1455            printable_line_len = if line_slice.chars().last() == Some('\n') {
1456                line_len_chars.saturating_sub(1)
1457            } else {
1458                line_len_chars
1459            };
1460            printable_line_end_char_idx = line_start_char + printable_line_len;
1461            // Start at the beginning of the new line and move to the first non-whitespace
1462            // char (the start of the first word). This is the desired behavior when
1463            // ctrl+right is pressed at the visual end-of-line.
1464            let mut next_char_idx = self.pos_to_char(pos);
1465            while next_char_idx < end_char
1466                && self
1467                    .text
1468                    .char(self.text.char_to_byte_idx(next_char_idx))
1469                    .is_whitespace()
1470            {
1471                next_char_idx += 1;
1472                if next_char_idx >= printable_line_end_char_idx {
1473                    return Position::new(pos.line, printable_line_len);
1474                }
1475            }
1476            if next_char_idx >= end_char {
1477                return pos;
1478            }
1479            return self.byte_to_pos(self.text.char_to_byte_idx(next_char_idx));
1480        }
1481
1482        // Phase 1: skip whitespace starting at the current char. If skipping would
1483        // cross the line boundary, clamp to end-of-line instead of crossing.
1484        while char_idx < end_char
1485            && self
1486                .text
1487                .char(self.text.char_to_byte_idx(char_idx))
1488                .is_whitespace()
1489        {
1490            char_idx += 1;
1491            if char_idx >= printable_line_end_char_idx {
1492                return Position::new(pos.line, printable_line_len);
1493            }
1494        }
1495
1496        if char_idx >= end_char {
1497            return pos;
1498        }
1499
1500        // Identify token type at the current position.
1501        let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
1502        let is_punct = first_char.is_ascii_punctuation();
1503
1504        // Phase 2: advance over the token. If advancing reaches or crosses the
1505        // line end, clamp to end-of-line.
1506        while char_idx < end_char {
1507            let c = self.text.char(self.text.char_to_byte_idx(char_idx));
1508            if is_punct {
1509                if !c.is_ascii_punctuation() {
1510                    break;
1511                }
1512            } else if c.is_whitespace() || c.is_ascii_punctuation() {
1513                break;
1514            }
1515            char_idx += 1;
1516            if char_idx >= printable_line_end_char_idx {
1517                return Position::new(pos.line, printable_line_len);
1518            }
1519        }
1520
1521        if skip_whitespace_after {
1522            while char_idx < end_char
1523                && self
1524                    .text
1525                    .char(self.text.char_to_byte_idx(char_idx))
1526                    .is_whitespace()
1527            {
1528                char_idx += 1;
1529                if char_idx >= printable_line_end_char_idx {
1530                    return Position::new(pos.line, printable_line_len);
1531                }
1532            }
1533        }
1534
1535        // If we've advanced past the line end due to some edge-case, clamp.
1536        if char_idx >= printable_line_end_char_idx {
1537            return Position::new(pos.line, printable_line_len);
1538        }
1539
1540        self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
1541    }
1542
1543    pub fn select_all(&mut self) {
1544        let last_line = self.text.len_lines(self.line_type()).saturating_sub(1);
1545        let pos = Position::new(
1546            last_line,
1547            self.text.line(last_line, self.line_type()).len_chars(),
1548        );
1549        self.selection = Some(Selection::new(Position::new(0, 0), pos));
1550        self.cursor = pos;
1551    }
1552
1553    pub fn selected_text(&self) -> String {
1554        if let Some(sel) = self.selection {
1555            let start = self.pos_to_byte(sel.min());
1556            let end = self.pos_to_byte(sel.max());
1557            self.text.slice(start..end).into()
1558        } else {
1559            String::new()
1560        }
1561    }
1562
1563    pub fn delete_selection(&mut self) -> String {
1564        if let Some(sel) = self.selection {
1565            let start = sel.min();
1566            let end = sel.max();
1567            let text = self.slice(start..end);
1568            self.delete_range(start, end);
1569            text
1570        } else {
1571            String::new()
1572        }
1573    }
1574
1575    pub fn undo(&mut self) -> bool {
1576        let inverse = {
1577            match self.history.undo() {
1578                Some(node) => node.inverse.clone(),
1579                None => return false,
1580            }
1581        };
1582        apply_changeset(self, &inverse);
1583        true
1584    }
1585
1586    pub fn redo(&mut self) -> bool {
1587        let change = {
1588            match self.history.redo() {
1589                Some(node) => node.change.clone(),
1590                None => return false,
1591            }
1592        };
1593        apply_changeset(self, &change);
1594        true
1595    }
1596
1597    fn export_history_snapshots(&self) -> (Vec<SerializableSnapshot>, Vec<SerializableSnapshot>) {
1598        // Get a copy of the timeline (change, inverse, version) triples.
1599        let entries = self.history.timeline_entries();
1600        let cursor = self.history.current_position();
1601
1602        // Start from the current buffer state.
1603        let mut temp = Buffer::from_lines(self.lines(), self.path.clone());
1604        temp.cursor = self.cursor;
1605        temp.selection = self.selection;
1606        temp.dirty = self.dirty;
1607        temp.version = self.version;
1608
1609        // Build undo snapshots by walking backwards, applying inverses.
1610        let mut undo_rev: Vec<SerializableSnapshot> = Vec::new();
1611        for i in (0..cursor).rev() {
1612            let (change, inverse, version) = &entries[i];
1613            temp.set_cursor(change.after_cursor);
1614            temp.set_selection(change.after_selection);
1615            let snap = SerializableSnapshot {
1616                lines: temp.lines(),
1617                cursor: change.after_cursor,
1618                selection: change.after_selection,
1619                dirty: temp.dirty,
1620                version: *version,
1621            };
1622            undo_rev.push(snap);
1623            // Revert one step.
1624            apply_changeset(&mut temp, inverse);
1625        }
1626
1627        // Temp now represents the base state before any undo/redo entries. Include it.
1628        let base_snap = SerializableSnapshot {
1629            lines: temp.lines(),
1630            cursor: temp.cursor,
1631            selection: temp.selection,
1632            dirty: temp.dirty,
1633            version: temp.version,
1634        };
1635        undo_rev.push(base_snap);
1636        undo_rev.reverse();
1637
1638        // Build redo snapshots by replaying forward from the base.
1639        let mut redo: Vec<SerializableSnapshot> = Vec::new();
1640        for (change, _inv, version) in entries.iter().skip(cursor) {
1641            apply_changeset(&mut temp, change);
1642            let snap = SerializableSnapshot {
1643                lines: temp.lines(),
1644                cursor: change.after_cursor,
1645                selection: change.after_selection,
1646                dirty: temp.dirty,
1647                version: *version,
1648            };
1649            redo.push(snap);
1650        }
1651
1652        (undo_rev, redo)
1653    }
1654
1655    pub fn undo_stack(&self) -> Vec<SerializableSnapshot> {
1656        let (undo, _redo) = self.export_history_snapshots();
1657        undo
1658    }
1659
1660    pub fn redo_stack(&self) -> Vec<SerializableSnapshot> {
1661        let (_undo, redo) = self.export_history_snapshots();
1662        redo
1663    }
1664
1665    pub fn save(&mut self, settings: &crate::settings::Settings) -> Result<()> {
1666        let path = self
1667            .path
1668            .clone()
1669            .ok_or_else(|| anyhow!("No path associated with buffer"))?;
1670
1671        self.is_saving = true;
1672        let result = self.do_save(&path, settings);
1673        self.is_saving = false;
1674        result
1675    }
1676
1677    fn do_save(&mut self, path: &PathBuf, settings: &crate::settings::Settings) -> Result<()> {
1678        let should_trim = *adapters::editor::trim_on_save(settings);
1679
1680        let mut text = self.text.clone();
1681
1682        if should_trim {
1683            let content = text.to_string();
1684            let had_trailing_newline = content.ends_with('\n');
1685            let trimmed: String = content
1686                .lines()
1687                .map(|l| l.trim_end().to_string())
1688                .collect::<Vec<_>>()
1689                .join("\n");
1690
1691            let trailing_empty = trimmed.lines().rev().take_while(|l| l.is_empty()).count();
1692            let final_lines: String = if trailing_empty > 1 {
1693                trimmed
1694                    .lines()
1695                    .take(trimmed.lines().count() - (trailing_empty - 1))
1696                    .collect::<Vec<_>>()
1697                    .join("\n")
1698            } else {
1699                trimmed.clone()
1700            };
1701
1702            let final_with_newline = if had_trailing_newline && !final_lines.ends_with('\n') {
1703                format!("{}\n", final_lines)
1704            } else {
1705                final_lines
1706            };
1707
1708            text = Rope::from_str(&final_with_newline);
1709            self.text = text.clone();
1710            self.normalize_cursor();
1711        }
1712
1713        // Normalize trailing newlines: if the content has at least one trailing
1714        // newline, keep exactly one; if it has none, don't add one.
1715        let content = {
1716            let s = text.to_string();
1717            let without_trailing = s.trim_end_matches('\n');
1718            if s.ends_with('\n') {
1719                format!("{}\n", without_trailing)
1720            } else {
1721                without_trailing.to_string()
1722            }
1723        };
1724
1725        let tmp = path.with_extension("tmp");
1726        fs::write(&tmp, &content)?;
1727        fs::rename(&tmp, path).or_else(|_| {
1728            fs::remove_file(path)?;
1729            fs::rename(&tmp, path)
1730        })?;
1731
1732        self.file_hash = Self::compute_hash(&text);
1733        self.dirty = false;
1734        if let Some((mtime, size)) = Self::get_file_metadata(path) {
1735            self.file_mtime = Some(mtime);
1736            self.file_size = Some(size);
1737        }
1738        Ok(())
1739    }
1740
1741    pub fn compute_hash(text: &Rope) -> Option<u64> {
1742        use std::collections::hash_map::DefaultHasher;
1743        use std::hash::{Hash, Hasher};
1744        let mut hasher = DefaultHasher::new();
1745        text.hash(&mut hasher);
1746        Some(hasher.finish())
1747    }
1748
1749    pub fn compute_file_hash(path: &Path) -> Option<u64> {
1750        use std::collections::hash_map::DefaultHasher;
1751        use std::hash::{Hash, Hasher};
1752        use std::io::Read;
1753        let mut file = fs::File::open(path).ok()?;
1754        let mut contents = Vec::new();
1755        file.read_to_end(&mut contents).ok()?;
1756        let mut hasher = DefaultHasher::new();
1757        contents.hash(&mut hasher);
1758        Some(hasher.finish())
1759    }
1760
1761    pub fn get_file_metadata(path: &Path) -> Option<(u64, u64)> {
1762        let metadata = fs::metadata(path).ok()?;
1763        let mtime = metadata
1764            .modified()
1765            .ok()?
1766            .duration_since(std::time::UNIX_EPOCH)
1767            .ok()?
1768            .as_secs();
1769        let size = metadata.len();
1770        Some((mtime, size))
1771    }
1772
1773    pub fn compute_and_store_file_hash(&mut self) {
1774        if let Some(ref path) = self.path {
1775            self.file_hash = Self::compute_file_hash(path);
1776            if let Some((mtime, size)) = Self::get_file_metadata(path) {
1777                self.file_mtime = Some(mtime);
1778                self.file_size = Some(size);
1779            }
1780        }
1781    }
1782
1783    pub fn check_external_modification(&mut self) -> bool {
1784        if self.is_saving {
1785            return false;
1786        }
1787        if self.is_dirty() {
1788            return false;
1789        }
1790        if let Some(ref path) = self.path
1791            && let Some((current_mtime, current_size)) = Self::get_file_metadata(path) {
1792                let mtime_changed = self.file_mtime.map(|m| m != current_mtime).unwrap_or(false);
1793                let size_changed = self.file_size.map(|s| s != current_size).unwrap_or(false);
1794                if !mtime_changed && !size_changed {
1795                    return false;
1796                }
1797                if let Some(current_hash) = Self::compute_file_hash(path)
1798                    && let Some(saved_hash) = self.file_hash
1799                        && current_hash != saved_hash {
1800                            self.external_modification_detected = true;
1801                            return true;
1802                        }
1803            }
1804        false
1805    }
1806
1807    pub fn reload_from_disk(&mut self) -> Result<()> {
1808        let path = self
1809            .path
1810            .clone()
1811            .ok_or_else(|| anyhow!("No path associated with buffer"))?;
1812        let content = fs::read_to_string(&path)?;
1813        self.text = Rope::from_str(&content);
1814        self.file_hash = Self::compute_hash(&self.text);
1815        if let Some((mtime, size)) = Self::get_file_metadata(&path) {
1816            self.file_mtime = Some(mtime);
1817            self.file_size = Some(size);
1818        }
1819        self.external_modification_detected = false;
1820        self.normalize_cursor();
1821        Ok(())
1822    }
1823
1824    pub fn clear_external_modification(&mut self) {
1825        self.external_modification_detected = false;
1826    }
1827
1828    pub fn scroll_to_cursor(&mut self, height: usize) {
1829        if height == 0 {
1830            return;
1831        }
1832        if self.cursor.line < self.scroll {
1833            self.scroll = self.cursor.line;
1834        } else if self.cursor.line >= self.scroll + height {
1835            self.scroll = self.cursor.line + 1 - height;
1836        }
1837    }
1838
1839    pub fn scroll_to_cursor_visual(
1840        &mut self,
1841        height: usize,
1842        _content_width: usize,
1843        _indent_width: usize,
1844    ) {
1845        self.scroll_to_cursor(height);
1846    }
1847
1848    pub fn scroll_x_to_cursor(&mut self, _content_width: usize, _indent_width: usize) {}
1849
1850    pub fn slice(&self, range: Range<Position>) -> String {
1851        let start_byte = self.pos_to_byte(range.start);
1852        let end_byte = self.pos_to_byte(range.end);
1853        self.text.slice(start_byte..end_byte).into()
1854    }
1855
1856    pub fn text(&self) -> String {
1857        self.text.to_string()
1858    }
1859
1860    pub fn apply_op_without_history(&mut self, op: &BufferOp) {
1861        match op {
1862            BufferOp::Replace { range, text, old_text, .. } => {
1863                // Compute byte indices for the requested range
1864                let mut start_byte = self.pos_to_byte(range.start);
1865                let end_byte = self.pos_to_byte(range.end).min(self.text.len());
1866
1867                let mut removed = false;
1868
1869                if start_byte < end_byte {
1870                    // Normal case: remove the existing range then insert the replacement
1871                    let range_len = end_byte - start_byte;
1872                    let old_len = old_text.len();
1873                    if old_len > range_len && !old_text.is_empty() {
1874                        // Computed end is shorter than the expected old text length.
1875                        // Try to find the full old_text starting at or after start_byte
1876                        let tail: String = self.text.slice(start_byte..self.text.len()).into();
1877                        if let Some(pos) = tail.find(old_text.as_str()) {
1878                            let found_end_byte = start_byte + pos + old_text.len();
1879                            self.text.remove(start_byte..found_end_byte);
1880                            removed = true;
1881                        } else {
1882                            // Fallback: remove the computed range and mark removed.
1883                            self.text.remove(start_byte..end_byte);
1884                            removed = true;
1885                        }
1886                    } else {
1887                        self.text.remove(start_byte..end_byte);
1888                        removed = true;
1889                    }
1890                } else {
1891                    // Defensive fallback: when computed end <= start, attempt to locate
1892                    // the expected old_text in the buffer and remove it to avoid
1893                    // duplicating the tail when the range positions are stale or incorrect.
1894                    if !old_text.is_empty() {
1895                        // Try to find old_text at or after start_byte
1896                        let tail: String = self.text.slice(start_byte..self.text.len()).into();
1897                        if let Some(pos) = tail.find(old_text.as_str()) {
1898                            let found_end_byte = start_byte + pos + old_text.len();
1899                            self.text.remove(start_byte..found_end_byte);
1900                            removed = true;
1901                        } else {
1902                            // Fallback: search whole buffer for first occurrence
1903                            let whole = self.text.to_string();
1904                            if let Some(pos) = whole.find(old_text.as_str()) {
1905                                self.text.remove(pos..(pos + old_text.len()));
1906                                start_byte = pos;
1907                                removed = true;
1908                            }
1909                        }
1910                    }
1911                }
1912
1913                // Only insert the replacement if we removed the expected old_text,
1914                // or if the operation is an insertion (old_text empty).
1915                if removed || old_text.is_empty() {
1916                    self.text.insert(start_byte, text);
1917                } else {
1918                    // Could not find old_text to remove; skip insertion to avoid duplicating content.
1919                }
1920            }
1921            BufferOp::MoveCursor { position } => {
1922                self.set_cursor(*position);
1923            }
1924            BufferOp::SetSelection { selection } => {
1925                self.set_selection(*selection);
1926            }
1927        }
1928    }
1929
1930}
1931
1932impl Default for Buffer {
1933    fn default() -> Self {
1934        Self::new()
1935    }
1936}
1937
1938impl From<&Rope> for Buffer {
1939    fn from(rope: &Rope) -> Self {
1940        let mut buf = Self::new();
1941        buf.text = rope.clone();
1942        buf
1943    }
1944}
1945
1946#[cfg(test)]
1947mod tests {
1948    use super::*;
1949
1950    #[test]
1951    fn test_position_mapping() {
1952        let buf = Buffer::from_text("hello\nworld\n");
1953
1954        assert_eq!(buf.byte_to_pos(0), Position::new(0, 0));
1955        assert_eq!(buf.byte_to_pos(5), Position::new(0, 5));
1956        assert_eq!(buf.byte_to_pos(6), Position::new(1, 0));
1957        assert_eq!(buf.byte_to_pos(11), Position::new(1, 5));
1958
1959        assert_eq!(buf.pos_to_byte(Position::new(0, 0)), 0);
1960        assert_eq!(buf.pos_to_byte(Position::new(0, 5)), 5);
1961        assert_eq!(buf.pos_to_byte(Position::new(1, 0)), 6);
1962        assert_eq!(buf.pos_to_byte(Position::new(1, 5)), 11);
1963    }
1964
1965    #[test]
1966    fn test_insert() {
1967        let mut buf = Buffer::from_text("hello");
1968        buf.cursor = Position::new(0, 5);
1969        buf.insert(" world");
1970        assert_eq!(buf.text.to_string(), "hello world");
1971    }
1972
1973    #[test]
1974    fn test_insert_newline() {
1975        let mut buf = Buffer::from_text("hello");
1976        buf.cursor = Position::new(0, 5);
1977        buf.insert_newline();
1978        assert_eq!(buf.text.to_string(), "hello\n");
1979        assert_eq!(buf.cursor, Position::new(1, 0));
1980    }
1981
1982    #[test]
1983    fn test_delete_backward() {
1984        let mut buf = Buffer::from_text("hello world");
1985        buf.cursor = Position::new(0, 5);
1986        buf.delete_backward();
1987        assert_eq!(buf.text.to_string(), "hell world");
1988        assert_eq!(buf.cursor, Position::new(0, 4));
1989    }
1990
1991    #[test]
1992    fn test_delete_selection() {
1993        let mut buf = Buffer::from_text("hello world");
1994        buf.selection = Some(Selection::new(Position::new(0, 0), Position::new(0, 6)));
1995        buf.delete_selection();
1996        assert_eq!(buf.text.to_string(), "world");
1997    }
1998
1999    #[test]
2000    fn test_cursor_movement() {
2001        let mut buf = Buffer::from_text("hello\nworld");
2002
2003        buf.cursor = Position::new(0, 3);
2004        assert_eq!(buf.cursor_right(), Position::new(0, 4));
2005        assert_eq!(buf.cursor_left(), Position::new(0, 3));
2006
2007        buf.cursor = Position::new(0, 0);
2008        assert_eq!(buf.cursor_up(), Position::new(0, 0));
2009
2010        buf.cursor = Position::new(0, 3);
2011        buf.cursor_up();
2012        assert_eq!(buf.cursor.line, 0);
2013
2014        buf.cursor = Position::new(1, 3);
2015        buf.cursor_up();
2016        assert_eq!(buf.cursor.line, 0);
2017        assert_eq!(buf.cursor.column, 3);
2018    }
2019
2020    #[test]
2021    fn test_undo_multiple_steps() {
2022        let mut buf = Buffer::from_text("");
2023
2024        buf.insert("a");
2025        buf.insert("b");
2026        buf.insert("c");
2027
2028        assert_eq!(buf.text(), "abc");
2029        // fast consecutive inserts of same kind coalesce into one entry
2030        assert_eq!(buf.history_position(), 1);
2031
2032        assert!(buf.undo());
2033        assert_eq!(buf.text(), "");
2034
2035        assert!(!buf.undo());
2036    }
2037
2038    #[test]
2039    fn test_undo_stack_persist_and_restore() {
2040        use std::path::PathBuf;
2041        // Use the no-merge constructor so each insert becomes its own undo step.
2042        let mut buf = Buffer::from_text_no_merge("");
2043
2044        buf.begin_transaction(ChangeKind::InsertText);
2045        buf.insert("a");
2046        buf.end_transaction();
2047
2048        buf.begin_transaction(ChangeKind::InsertText);
2049        buf.insert("b");
2050        buf.end_transaction();
2051
2052        assert_eq!(buf.text(), "ab");
2053
2054        let undo_stack = buf.undo_stack();
2055        let redo_stack = buf.redo_stack();
2056
2057        let restored = Buffer::restore(
2058            PathBuf::from("dummy"),
2059            buf.lines(),
2060            buf.selection,
2061            buf.scroll,
2062            buf.dirty,
2063            buf.markers.clone(),
2064            undo_stack.clone(),
2065            redo_stack.clone(),
2066        );
2067        let mut r = restored;
2068
2069        // Undo twice to step back through the history
2070        assert!(r.undo());
2071        assert_eq!(r.text(), "a");
2072
2073        assert!(r.undo());
2074        assert_eq!(r.text(), "");
2075    }
2076
2077    #[test]
2078    fn test_reopen_and_excess_undos_do_not_duplicate_tail() {
2079        use tempfile::tempdir;
2080        use crate::project::Project;
2081        use crate::editor::fold::FoldState;
2082        use crate::editor::history::ChangeKind;
2083        use std::fs;
2084
2085        let tmp = tempdir().expect("tempdir");
2086        let project_path = tmp.path().to_path_buf();
2087        let file_a = project_path.join("a.txt");
2088        let file_b = project_path.join("b.txt");
2089
2090        fs::write(&file_a, "alpha\nbeta\n").expect("write a");
2091        fs::write(&file_b, "other\n").expect("write b");
2092
2093        let mut project = Project::new(project_path).expect("project new");
2094
2095        // open and edit A
2096        let mut buf_a = project.take_buffer(file_a.clone()).expect("take A");
2097        buf_a.begin_transaction(ChangeKind::InsertText);
2098        buf_a.insert_text_raw("// edit1\n");
2099        buf_a.end_transaction();
2100
2101        buf_a.begin_transaction(ChangeKind::InsertText);
2102        buf_a.insert_text_raw("// edit2\n");
2103        buf_a.end_transaction();
2104
2105        project.stash_buffer(buf_a, FoldState::default());
2106
2107        let buf_b = project.take_buffer(file_b.clone()).expect("take B");
2108        project.stash_buffer(buf_b, FoldState::default());
2109
2110        let mut buf_a2 = project.take_buffer(file_a.clone()).expect("reopen A");
2111
2112        let undo_snapshots = buf_a2.undo_stack().len();
2113        for _ in 0..(undo_snapshots + 3) {
2114            let _ = buf_a2.undo();
2115        }
2116
2117        let expected = fs::read_to_string(&file_a).expect("read file A");
2118        assert_eq!(buf_a2.text(), expected);
2119    }
2120
2121    #[test]
2122    fn test_undo_redo_multiple_steps() {
2123        let mut buf = Buffer::from_text("");
2124
2125        buf.insert("a");
2126        buf.insert("b");
2127
2128        assert_eq!(buf.text(), "ab");
2129        assert_eq!(buf.history_position(), 1);
2130
2131        buf.undo();
2132        assert_eq!(buf.text(), "");
2133
2134        buf.redo();
2135        assert_eq!(buf.text(), "ab");
2136
2137        assert!(!buf.redo());
2138    }
2139
2140    #[test]
2141    fn test_undo_after_new_edit() {
2142        let mut buf = Buffer::from_text("");
2143
2144        buf.insert("a");
2145        buf.insert("b");
2146
2147        // "a" and "b" merge → one undo clears both
2148        buf.undo();
2149        assert_eq!(buf.text(), "");
2150
2151        buf.insert("c");
2152        assert_eq!(buf.text(), "c");
2153
2154        buf.undo();
2155        assert_eq!(buf.text(), "");
2156    }
2157
2158    #[test]
2159    fn test_line_access() {
2160        let buf = Buffer::from_text("hello\nworld\n");
2161
2162        assert_eq!(buf.line_count(), 3);
2163        assert_eq!(buf.line(0).unwrap().to_string(), "hello");
2164        assert_eq!(buf.line(1).unwrap().to_string(), "world");
2165    }
2166
2167    #[test]
2168    fn test_cursor_clamping() {
2169        let mut buf = Buffer::from_text("hello");
2170        buf.cursor = Position::new(100, 100);
2171        buf.normalize_cursor();
2172
2173        assert!(buf.cursor.line < buf.line_count());
2174        assert!(buf.cursor.column <= buf.line(0).unwrap().chars().count());
2175    }
2176
2177    #[test]
2178    fn test_unicode() {
2179        let mut buf = Buffer::from_text("héllo\nwörld");
2180        assert_eq!(buf.line_count(), 2);
2181
2182        buf.cursor = Position::new(0, 6);
2183        buf.insert("!");
2184        let line0 = buf.text.line(0, LineType::LF).to_string();
2185        assert!(
2186            line0.starts_with("héllo"),
2187            "line0 should start with héllo, got: {}",
2188            line0
2189        );
2190    }
2191
2192    #[test]
2193    fn test_snapshot() {
2194        let buf = Buffer::from_text("hello");
2195        let snap = buf.snapshot();
2196        assert_eq!(snap.text.to_string(), "hello");
2197    }
2198
2199    #[test]
2200    fn test_delete_forward() {
2201        let mut buf = Buffer::from_text("hello");
2202        buf.cursor = Position::new(0, 4);
2203        buf.delete_forward();
2204        assert_eq!(buf.text.to_string(), "hell");
2205    }
2206
2207    #[test]
2208    fn test_empty_buffer() {
2209        let buf = Buffer::new();
2210        assert!(buf.is_empty());
2211        assert_eq!(buf.line_count(), 1);
2212        assert_eq!(buf.cursor, Position::new(0, 0));
2213    }
2214
2215    #[test]
2216    fn test_buffer_id() {
2217        let buf1 = Buffer::new();
2218        let buf2 = Buffer::new();
2219        assert_ne!(buf1.id(), buf2.id());
2220    }
2221
2222    #[test]
2223    fn test_version_increment() {
2224        let mut buf = Buffer::from_text("hello");
2225        let v1 = buf.version();
2226        buf.insert(" world");
2227        let v2 = buf.version();
2228        assert!(v2 > v1);
2229    }
2230
2231    #[test]
2232    fn test_fast_typing_merges_into_single_undo() {
2233        let mut buf = Buffer::from_text("");
2234
2235        buf.insert("h");
2236        buf.insert("e");
2237        buf.insert("l");
2238        buf.insert("l");
2239        buf.insert("o");
2240
2241        assert_eq!(buf.text(), "hello");
2242        // all five chars are adjacent and fast → one history entry
2243        assert_eq!(buf.history_position(), 1);
2244
2245        buf.undo();
2246        assert_eq!(buf.text(), "");
2247    }
2248
2249    #[test]
2250    fn test_fast_backspace_merges_into_single_undo() {
2251        let mut buf = Buffer::from_text("hello");
2252        buf.set_cursor(Position::new(0, 5));
2253
2254        buf.delete_backward();
2255        buf.delete_backward();
2256        buf.delete_backward();
2257
2258        assert_eq!(buf.text(), "he");
2259        assert_eq!(buf.history_position(), 1);
2260
2261        buf.undo();
2262        assert_eq!(buf.text(), "hello");
2263    }
2264
2265    #[test]
2266    fn test_structural_ops_break_merge_chain() {
2267        let mut buf = Buffer::from_text("");
2268
2269        buf.insert("a");
2270        buf.insert("b"); // merges with "a" → 1 entry
2271        buf.insert_newline(); // Structural → new entry
2272        buf.insert("c"); // InsertText → new entry (different from Structural)
2273
2274        assert_eq!(buf.text(), "ab\nc");
2275        // "ab" (merged), newline, "c" → 3 entries
2276        assert_eq!(buf.history_position(), 3);
2277
2278        buf.undo();
2279        assert_eq!(buf.text(), "ab\n");
2280
2281        buf.undo();
2282        assert_eq!(buf.text(), "ab");
2283
2284        buf.undo();
2285        assert_eq!(buf.text(), "");
2286        buf.undo();
2287        assert_eq!(buf.text(), "");
2288    }
2289
2290    #[test]
2291    fn test_mixed_undo_steps() {
2292        let mut buf = Buffer::from_text("");
2293
2294        buf.insert("hello");
2295        buf.insert_newline();
2296        buf.insert("world");
2297
2298        buf.undo();
2299        assert_eq!(buf.text(), "hello\n");
2300
2301        buf.undo();
2302        assert_eq!(buf.text(), "hello");
2303
2304        buf.undo();
2305        assert_eq!(buf.text(), "");
2306    }
2307
2308    #[test]
2309    fn test_cross_line_insert_over_selection() {
2310        let mut buf = Buffer::from_text("hello\nworld");
2311        buf.set_selection(Some(Selection::new(
2312            Position::new(0, 2),
2313            Position::new(1, 3),
2314        )));
2315        buf.insert("X");
2316        assert_eq!(buf.text(), "heXld");
2317    }
2318
2319    #[test]
2320    fn test_cross_line_delete_backward_with_selection() {
2321        let mut buf = Buffer::from_text("hello\nworld");
2322        buf.set_selection(Some(Selection::new(
2323            Position::new(0, 5),
2324            Position::new(1, 0),
2325        )));
2326        buf.delete_backward();
2327        assert_eq!(buf.text(), "helloworld");
2328    }
2329
2330    #[test]
2331    fn test_cross_line_selection_delete_all() {
2332        let mut buf = Buffer::from_text("abc\ndef\nghi");
2333        buf.set_selection(Some(Selection::new(
2334            Position::new(0, 0),
2335            Position::new(2, 3),
2336        )));
2337        buf.insert("X");
2338        assert_eq!(buf.text(), "X");
2339    }
2340
2341    #[test]
2342    fn test_cross_line_selection_undo_redo() {
2343        let mut buf = Buffer::from_text("hello\nworld");
2344        // Selection: anchor=(0,3), active=(1,2), cursor=(1,2)
2345        buf.set_selection(Some(Selection::new(
2346            Position::new(0, 3),
2347            Position::new(1, 2),
2348        )));
2349        buf.insert("X");
2350        assert_eq!(buf.text(), "helXrld");
2351        assert_eq!(buf.cursor(), Position::new(0, 4)); // after "helX"
2352        buf.undo();
2353        assert_eq!(buf.text(), "hello\nworld");
2354        // Undo restores cursor to before_cursor = (1,2) (active end of original selection)
2355        assert_eq!(buf.cursor(), Position::new(1, 2));
2356        buf.redo();
2357        assert_eq!(buf.text(), "helXrld");
2358        assert_eq!(buf.cursor(), Position::new(0, 4));
2359    }
2360
2361    #[test]
2362    fn test_offset_up_excludes_trailing_newline() {
2363        let buf = Buffer::from_text("hello\nworld");
2364        let pos = buf.offset_up(Position::new(1, 5));
2365        assert_eq!(pos, Position::new(0, 5));
2366    }
2367
2368    #[test]
2369    fn test_offset_up_clamps_to_shorter_line() {
2370        let buf = Buffer::from_text("hi\nworld");
2371        let pos = buf.offset_up(Position::new(1, 5));
2372        assert_eq!(pos, Position::new(0, 2));
2373    }
2374
2375    #[test]
2376    fn test_delete_forward_multibyte() {
2377        let mut buf = Buffer::from_text("αβγ");
2378        buf.set_cursor(Position::new(0, 1));
2379        buf.delete_forward();
2380        assert_eq!(buf.text(), "αγ");
2381    }
2382
2383    #[test]
2384    fn test_delete_forward_newline() {
2385        let mut buf = Buffer::from_text("abc\ndef");
2386        buf.set_cursor(Position::new(0, 3));
2387        buf.delete_forward();
2388        assert_eq!(buf.text(), "abcdef");
2389    }
2390
2391    #[test]
2392    fn test_pos_to_char_and_char_to_pos() {
2393        let buf = Buffer::from_text("hello\nworld");
2394        assert_eq!(buf.pos_to_char(Position::new(0, 3)), 3);
2395        // "hello\n" = 6 chars, so line 1 col 2 = char 8
2396        assert_eq!(buf.pos_to_char(Position::new(1, 2)), 8);
2397        assert_eq!(buf.char_to_pos(3), Position::new(0, 3));
2398        assert_eq!(buf.char_to_pos(8), Position::new(1, 2));
2399    }
2400
2401    #[test]
2402    fn test_set_selection_normalizes_positions() {
2403        let mut buf = Buffer::from_text("hi\nworld");
2404        buf.set_selection(Some(Selection::new(
2405            Position::new(0, 100), // "hi" has only 2 chars
2406            Position::new(1, 3),
2407        )));
2408        let sel = buf.selection().unwrap();
2409        assert_eq!(sel.anchor, Position::new(0, 2));
2410        assert_eq!(sel.active, Position::new(1, 3));
2411    }
2412
2413    #[test]
2414    fn test_newline_undo_roundtrip() {
2415        let mut buf = Buffer::from_text("helloworld");
2416        buf.set_cursor(Position::new(0, 5));
2417        buf.insert_newline();
2418        assert_eq!(buf.text(), "hello\nworld");
2419        assert_eq!(buf.cursor(), Position::new(1, 0));
2420        buf.undo();
2421        assert_eq!(buf.text(), "helloworld");
2422        assert_eq!(buf.cursor(), Position::new(0, 5));
2423    }
2424
2425    // ── insert_at ──────────────────────────────────────────────────────────
2426
2427    #[test]
2428    fn test_insert_at_advances_cursor_when_at_position() {
2429        let mut buf = Buffer::from_text("hello");
2430        buf.set_cursor(Position::new(0, 5));
2431        buf.insert_at(Position::new(0, 5), " world");
2432        assert_eq!(buf.text(), "hello world");
2433        assert_eq!(buf.cursor(), Position::new(0, 11));
2434    }
2435
2436    #[test]
2437    fn test_insert_at_does_not_move_cursor_when_before_cursor() {
2438        let mut buf = Buffer::from_text("world");
2439        buf.set_cursor(Position::new(0, 5));
2440        buf.insert_at(Position::new(0, 0), "hello ");
2441        assert_eq!(buf.text(), "hello world");
2442        assert_eq!(buf.cursor(), Position::new(0, 5));
2443    }
2444
2445    // ── selected_text ──────────────────────────────────────────────────────
2446
2447    #[test]
2448    fn test_selected_text_single_line() {
2449        let mut buf = Buffer::from_text("hello world");
2450        buf.set_selection(Some(Selection {
2451            anchor: Position::new(0, 0),
2452            active: Position::new(0, 5),
2453        }));
2454        assert_eq!(buf.selected_text(), "hello");
2455    }
2456
2457    #[test]
2458    fn test_selected_text_multiline() {
2459        let mut buf = Buffer::from_text("hello\nworld");
2460        buf.set_selection(Some(Selection {
2461            anchor: Position::new(0, 3),
2462            active: Position::new(1, 3),
2463        }));
2464        assert_eq!(buf.selected_text(), "lo\nwor");
2465    }
2466
2467    #[test]
2468    fn test_selected_text_empty_when_no_selection() {
2469        let buf = Buffer::from_text("hello");
2470        assert_eq!(buf.selected_text(), "");
2471    }
2472
2473    // ── word_range_at ─────────────────────────────────────────────────────
2474
2475    #[test]
2476    fn test_word_range_at_middle_of_word() {
2477        let buf = Buffer::from_text("hello world");
2478        let (start, end) = buf.word_range_at(Position::new(0, 2));
2479        // Position 2 is in the middle of "hello", so start is 2, end is 5
2480        assert_eq!(start, 2);
2481        assert_eq!(end, 5);
2482    }
2483
2484    #[test]
2485    fn test_word_range_at_on_space() {
2486        let buf = Buffer::from_text("hello world");
2487        // Space is not a word character — just ensure no panic
2488        let _ = buf.word_range_at(Position::new(0, 5));
2489    }
2490
2491    // ── delete_range edge cases ───────────────────────────────────────────
2492
2493    #[test]
2494    fn test_delete_range_same_position_is_noop() {
2495        let mut buf = Buffer::from_text("hello");
2496        let pos = Position::new(0, 2);
2497        buf.delete_range(pos, pos);
2498        assert_eq!(buf.text(), "hello");
2499    }
2500
2501    // ── char_count, current_line, slice ──────────────────────────────────
2502
2503    #[test]
2504    fn test_char_count() {
2505        let buf = Buffer::from_text("hello\nworld");
2506        assert_eq!(buf.char_count(), 11);
2507    }
2508
2509    #[test]
2510    fn test_current_line() {
2511        let mut buf = Buffer::from_text("hello\nworld");
2512        buf.set_cursor(Position::new(1, 0));
2513        assert_eq!(buf.current_line(), 1);
2514    }
2515
2516    #[test]
2517    fn test_slice() {
2518        let buf = Buffer::from_text("hello world");
2519        let s = buf.slice(Position::new(0, 0)..Position::new(0, 5));
2520        assert_eq!(s, "hello");
2521    }
2522
2523    // ── history_len ──────────────────────────────────────────────────────
2524
2525    #[test]
2526    fn test_history_len() {
2527        let mut buf = Buffer::from_text("");
2528        assert_eq!(buf.history_len(), 0);
2529        buf.insert("a");
2530        assert!(buf.history_len() > 0);
2531    }
2532
2533    // ── scroll_to_cursor ──────────────────────────────────────────────────
2534
2535    #[test]
2536    fn test_scroll_to_cursor_scrolls_down_when_cursor_below_view() {
2537        let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
2538        buf.set_cursor(Position::new(4, 0));
2539        buf.scroll_to_cursor(3);
2540        assert!(buf.scroll >= 2);
2541    }
2542
2543    #[test]
2544    fn test_scroll_to_cursor_scrolls_up_when_cursor_above_view() {
2545        let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
2546        buf.scroll = 4;
2547        buf.set_cursor(Position::new(0, 0));
2548        buf.scroll_to_cursor(3);
2549        assert_eq!(buf.scroll, 0);
2550    }
2551
2552    // ── offset_down / offset_down_n ──────────────────────────────────────
2553
2554    #[test]
2555    fn test_offset_down_clamps_column_to_shorter_line() {
2556        let buf = Buffer::from_text("hello world\nhi");
2557        let pos = buf.offset_down(Position::new(0, 8));
2558        assert_eq!(pos, Position::new(1, 2));
2559    }
2560
2561    #[test]
2562    fn test_offset_down_at_last_line_stays() {
2563        let buf = Buffer::from_text("hello");
2564        let pos = buf.offset_down(Position::new(0, 3));
2565        assert_eq!(pos, Position::new(0, 3));
2566    }
2567
2568    #[test]
2569    fn test_offset_down_n_stops_at_last_line() {
2570        let buf = Buffer::from_text("a\nb\nc");
2571        let pos = buf.offset_down_n(Position::new(0, 0), 100);
2572        assert_eq!(pos.line, 2);
2573    }
2574
2575    // ── offset_left / offset_right wrapping ──────────────────────────────
2576
2577    #[test]
2578    fn test_offset_left_wraps_to_end_of_previous_line() {
2579        let buf = Buffer::from_text("hello\nworld");
2580        let pos = buf.offset_left(Position::new(1, 0));
2581        assert_eq!(pos, Position::new(0, 5));
2582    }
2583
2584    #[test]
2585    fn test_offset_right_wraps_to_start_of_next_line() {
2586        let buf = Buffer::from_text("hello\nworld");
2587        let pos = buf.offset_right(Position::new(0, 5));
2588        assert_eq!(pos, Position::new(1, 0));
2589    }
2590
2591    // ── insert_text_raw CRLF normalization ───────────────────────────────
2592
2593    #[test]
2594    fn test_insert_text_raw_normalizes_crlf() {
2595        let mut buf = Buffer::from_text("");
2596        buf.insert_text_raw("hello\r\nworld");
2597        assert_eq!(buf.text(), "hello\nworld");
2598    }
2599
2600    #[test]
2601    fn test_insert_text_raw_normalizes_cr_only() {
2602        let mut buf = Buffer::from_text("");
2603        buf.insert_text_raw("hello\rworld");
2604        assert_eq!(buf.text(), "hello\nworld");
2605    }
2606
2607    // ── replace_range no-op branch ─────────────────────────────────────────
2608
2609    #[test]
2610    fn test_replace_range_same_text_at_cursor_is_noop_for_history() {
2611        let mut buf = Buffer::from_text("hello");
2612        buf.set_cursor(Position::new(0, 0));
2613        let before_history_pos = buf.history_position();
2614        buf.replace_range(Position::new(0, 0), Position::new(0, 5), "hello");
2615        assert_eq!(buf.history_position(), before_history_pos);
2616        assert_eq!(buf.text(), "hello");
2617    }
2618
2619    // ── delete_word_forward starting on whitespace ────────────────────────
2620
2621    #[test]
2622    fn test_delete_word_forward_from_whitespace() {
2623        let mut buf = Buffer::from_text("hello   world");
2624        buf.set_cursor(Position::new(0, 5));
2625        buf.delete_word_forward();
2626        assert_eq!(buf.text(), "helloworld");
2627    }
2628
2629    // ── delete_line on last line ───────────────────────────────────────────
2630
2631    #[test]
2632    fn test_delete_line_last_line_leaves_empty() {
2633        let mut buf = Buffer::from_text("hello");
2634        buf.set_cursor(Position::new(0, 0));
2635        buf.delete_line();
2636        assert_eq!(buf.line_count(), 1);
2637        assert_eq!(buf.line(0).unwrap(), "");
2638    }
2639
2640    // ── save: trailing-newline normalization ──────────────────────────────
2641
2642    /// Helper: create a settings instance that has `trim_on_save` set to the
2643    /// given value, using a temporary project directory so `Settings::new`
2644    /// succeeds without touching the real config.
2645    fn settings_with_trim(trim: bool) -> (crate::settings::Settings, tempfile::TempDir) {
2646        let dir = tempfile::tempdir().unwrap();
2647        let project_config = dir.path().join(".oo").join("config.yaml");
2648        std::fs::create_dir(dir.path().join(".oo")).unwrap();
2649        let override_yaml = format!("editor:\n  trim_on_save: {}\n", trim);
2650        std::fs::write(&project_config, &override_yaml).unwrap();
2651        let s = crate::settings::Settings::new(&project_config).unwrap();
2652        (s, dir)
2653    }
2654
2655    /// Save `content` to a temp file and return the bytes written to disk.
2656    fn save_content(content: &str, trim: bool) -> String {
2657        let (settings, dir) = settings_with_trim(trim);
2658        let file = dir.path().join("test.txt");
2659        let mut buf = Buffer::from_text(content);
2660        buf.path = Some(file.clone());
2661        buf.save(&settings).expect("save should succeed");
2662        std::fs::read_to_string(&file).expect("file should exist after save")
2663    }
2664
2665    #[test]
2666    fn save_no_trailing_newline_is_preserved() {
2667        // A file with no trailing newline must not gain one.
2668        let on_disk = save_content("hello\nworld", false);
2669        assert_eq!(on_disk, "hello\nworld");
2670    }
2671
2672    #[test]
2673    fn save_single_trailing_newline_is_preserved() {
2674        // A file with exactly one trailing newline must keep it.
2675        let on_disk = save_content("hello\nworld\n", false);
2676        assert_eq!(on_disk, "hello\nworld\n");
2677    }
2678
2679    #[test]
2680    fn save_multiple_trailing_newlines_collapsed_to_one() {
2681        // Two or more trailing newlines must be collapsed to exactly one.
2682        let on_disk = save_content("hello\nworld\n\n", false);
2683        assert_eq!(on_disk, "hello\nworld\n");
2684
2685        let on_disk = save_content("hello\nworld\n\n\n", false);
2686        assert_eq!(on_disk, "hello\nworld\n");
2687    }
2688
2689    #[test]
2690    fn save_trim_on_save_preserves_trailing_newline() {
2691        // With trim_on_save the file must still end with exactly one newline
2692        // when the original content has one.
2693        let on_disk = save_content("hello  \nworld  \n", true);
2694        assert_eq!(on_disk, "hello\nworld\n");
2695    }
2696
2697    #[test]
2698    fn save_trim_on_save_no_trailing_newline_not_added() {
2699        // With trim_on_save, if the original had no trailing newline, none is added.
2700        let on_disk = save_content("hello  \nworld  ", true);
2701        assert_eq!(on_disk, "hello\nworld");
2702    }
2703
2704    #[test]
2705    fn save_trim_on_save_collapses_extra_trailing_newlines() {
2706        let on_disk = save_content("hello\nworld\n\n\n", true);
2707        assert_eq!(on_disk, "hello\nworld\n");
2708    }
2709
2710    #[test]
2711    fn restore_clamps_stale_cursor_to_shorter_content() {
2712        // Simulates reopening a file whose line count decreased on disk.
2713        // The saved selection points to line 10, col 5 but the restored
2714        // content only has 3 lines — restore() must not panic and must
2715        // clamp the cursor to within bounds.
2716        use crate::editor::position::Position;
2717        use crate::editor::selection::Selection;
2718        let stale_selection = Some(Selection {
2719            anchor: Position::new(10, 3),
2720            active: Position::new(10, 5),
2721        });
2722        let short_lines = vec!["line0".to_owned(), "line1".to_owned(), "line2".to_owned()];
2723        let buf = Buffer::restore(
2724            std::path::PathBuf::from("/fake/path.rs"),
2725            short_lines,
2726            stale_selection,
2727            0,
2728            true, // dirty=true so lines are used as-is
2729            vec![],
2730            vec![],
2731            vec![],
2732        );
2733        // Cursor must be within the 3-line buffer.
2734        assert!(buf.cursor().line < 3, "cursor.line OOB: {}", buf.cursor().line);
2735        // Exercising key-movement helpers must not panic.
2736        let _ = buf.offset_right(buf.cursor());
2737        let _ = buf.offset_down(buf.cursor());
2738        let _ = buf.offset_line_end(buf.cursor());
2739    }
2740}