Skip to main content

oxihuman_core/
piece_table.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Piece table text buffer — stores original and added text in two buffers,
6//! with a sequence of pieces describing the logical document.
7
8/// Source of a piece.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PieceSource {
11    Original,
12    Added,
13}
14
15/// A piece describing a run of characters from a source buffer.
16#[derive(Debug, Clone)]
17pub struct Piece {
18    pub source: PieceSource,
19    pub start: usize,
20    pub length: usize,
21}
22
23/// Piece table text buffer.
24pub struct PieceTable {
25    original: Vec<char>,
26    added: Vec<char>,
27    pieces: Vec<Piece>,
28}
29
30impl PieceTable {
31    /// Create a piece table from an initial string.
32    pub fn new(initial: &str) -> Self {
33        let original: Vec<char> = initial.chars().collect();
34        let len = original.len();
35        let pieces = if len > 0 {
36            vec![Piece {
37                source: PieceSource::Original,
38                start: 0,
39                length: len,
40            }]
41        } else {
42            Vec::new()
43        };
44        Self {
45            original,
46            added: Vec::new(),
47            pieces,
48        }
49    }
50
51    /// Total character count of the document.
52    pub fn len(&self) -> usize {
53        self.pieces.iter().map(|p| p.length).sum()
54    }
55
56    /// True if the document is empty.
57    pub fn is_empty(&self) -> bool {
58        self.len() == 0
59    }
60
61    /// Insert `text` at logical character position `pos`.
62    pub fn insert(&mut self, pos: usize, text: &str) {
63        let add_start = self.added.len();
64        self.added.extend(text.chars());
65        let add_len = self.added.len() - add_start;
66        if add_len == 0 {
67            return;
68        }
69        let new_piece = Piece {
70            source: PieceSource::Added,
71            start: add_start,
72            length: add_len,
73        };
74        /* find the piece containing pos */
75        let mut cur = 0usize;
76        let mut insert_idx = self.pieces.len();
77        for (i, p) in self.pieces.iter().enumerate() {
78            if cur + p.length >= pos {
79                let offset = pos - cur;
80                if offset == 0 {
81                    insert_idx = i;
82                } else if offset == p.length {
83                    insert_idx = i + 1;
84                } else {
85                    /* split piece at offset */
86                    let left = Piece {
87                        source: p.source,
88                        start: p.start,
89                        length: offset,
90                    };
91                    let right = Piece {
92                        source: p.source,
93                        start: p.start + offset,
94                        length: p.length - offset,
95                    };
96                    self.pieces.remove(i);
97                    self.pieces.insert(i, right);
98                    self.pieces.insert(i, new_piece.clone());
99                    self.pieces.insert(i, left);
100                    return;
101                }
102                break;
103            }
104            cur += p.length;
105        }
106        self.pieces.insert(insert_idx, new_piece);
107    }
108
109    /// Collect the full document text.
110    pub fn as_string(&self) -> String {
111        let mut s = String::with_capacity(self.len());
112        for p in &self.pieces {
113            let src = match p.source {
114                PieceSource::Original => &self.original,
115                PieceSource::Added => &self.added,
116            };
117            s.extend(src[p.start..p.start + p.length].iter());
118        }
119        s
120    }
121
122    /// Number of pieces in the table.
123    pub fn piece_count(&self) -> usize {
124        self.pieces.len()
125    }
126}
127
128/// Create a piece table from initial text.
129pub fn new_piece_table(initial: &str) -> PieceTable {
130    PieceTable::new(initial)
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_initial_content() {
139        let pt = PieceTable::new("hello");
140        assert_eq!(pt.as_string(), "hello"); /* initial text preserved */
141    }
142
143    #[test]
144    fn test_len() {
145        let pt = PieceTable::new("abc");
146        assert_eq!(pt.len(), 3); /* correct length */
147    }
148
149    #[test]
150    fn test_is_empty() {
151        let pt = PieceTable::new("");
152        assert!(pt.is_empty()); /* empty initial */
153    }
154
155    #[test]
156    fn test_insert_at_end() {
157        let mut pt = PieceTable::new("hello");
158        pt.insert(5, " world");
159        assert_eq!(pt.as_string(), "hello world"); /* appended */
160    }
161
162    #[test]
163    fn test_insert_at_start() {
164        let mut pt = PieceTable::new("world");
165        pt.insert(0, "hello ");
166        assert_eq!(pt.as_string(), "hello world"); /* prepended */
167    }
168
169    #[test]
170    fn test_insert_in_middle() {
171        let mut pt = PieceTable::new("helo");
172        pt.insert(3, "l");
173        assert_eq!(pt.as_string(), "hello"); /* mid-insert */
174    }
175
176    #[test]
177    fn test_multiple_inserts() {
178        let mut pt = PieceTable::new("");
179        pt.insert(0, "c");
180        pt.insert(0, "b");
181        pt.insert(0, "a");
182        assert_eq!(pt.as_string(), "abc"); /* three prepends */
183    }
184
185    #[test]
186    fn test_piece_count_grows() {
187        let mut pt = PieceTable::new("ab");
188        pt.insert(1, "X");
189        assert!(pt.piece_count() > 1); /* split created more pieces */
190    }
191
192    #[test]
193    fn test_new_helper() {
194        let pt = new_piece_table("test");
195        assert_eq!(pt.as_string(), "test"); /* helper works */
196    }
197
198    #[test]
199    fn test_insert_empty_string() {
200        let mut pt = PieceTable::new("abc");
201        pt.insert(1, "");
202        assert_eq!(pt.as_string(), "abc"); /* no change for empty insert */
203    }
204}