1use unicode_width::UnicodeWidthStr;
2
3#[derive(Debug, Clone)]
4pub struct Editor {
5 content: String,
6 cursor: usize,
7 scroll_offset: usize,
8 vertical_scroll: usize,
9}
10
11impl Editor {
12 pub fn new() -> Self {
13 Self {
14 content: String::new(),
15 cursor: 0,
16 scroll_offset: 0,
17 vertical_scroll: 0,
18 }
19 }
20
21 pub fn with_content(content: String) -> Self {
22 let cursor = content.len();
23 Self {
24 content,
25 cursor,
26 scroll_offset: 0,
27 vertical_scroll: 0,
28 }
29 }
30
31 pub fn content(&self) -> &str {
32 &self.content
33 }
34
35 pub fn cursor(&self) -> usize {
36 self.cursor
37 }
38
39 pub fn scroll_offset(&self) -> usize {
40 self.scroll_offset
41 }
42
43 pub fn vertical_scroll(&self) -> usize {
44 self.vertical_scroll
45 }
46
47 pub fn cursor_line_col(&self) -> (usize, usize) {
49 let before = &self.content[..self.cursor];
50 let line = before.matches('\n').count();
51 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
52 let col = UnicodeWidthStr::width(&self.content[line_start..self.cursor]);
53 (line, col)
54 }
55
56 pub fn line_count(&self) -> usize {
57 self.content.matches('\n').count() + 1
58 }
59
60 fn line_start(&self, n: usize) -> usize {
62 if n == 0 {
63 return 0;
64 }
65 let mut count = 0;
66 for (i, c) in self.content.char_indices() {
67 if c == '\n' {
68 count += 1;
69 if count == n {
70 return i + 1;
71 }
72 }
73 }
74 self.content.len()
75 }
76
77 fn line_end(&self, n: usize) -> usize {
79 let start = self.line_start(n);
80 match self.content[start..].find('\n') {
81 Some(pos) => start + pos,
82 None => self.content.len(),
83 }
84 }
85
86 fn line_content(&self, n: usize) -> &str {
88 &self.content[self.line_start(n)..self.line_end(n)]
89 }
90
91 pub fn visual_cursor(&self) -> usize {
93 let (_, col) = self.cursor_line_col();
94 col.saturating_sub(self.scroll_offset)
95 }
96
97 pub fn insert_char(&mut self, c: char) {
98 self.content.insert(self.cursor, c);
99 self.cursor += c.len_utf8();
100 }
101
102 pub fn insert_newline(&mut self) {
103 self.content.insert(self.cursor, '\n');
104 self.cursor += 1;
105 }
106
107 pub fn delete_back(&mut self) {
108 if self.cursor > 0 {
109 let prev = self.prev_char_boundary();
110 self.content.drain(prev..self.cursor);
111 self.cursor = prev;
112 }
113 }
114
115 pub fn delete_forward(&mut self) {
116 if self.cursor < self.content.len() {
117 let next = self.next_char_boundary();
118 self.content.drain(self.cursor..next);
119 }
120 }
121
122 pub fn move_left(&mut self) {
123 if self.cursor > 0 {
124 self.cursor = self.prev_char_boundary();
125 }
126 }
127
128 pub fn move_right(&mut self) {
129 if self.cursor < self.content.len() {
130 self.cursor = self.next_char_boundary();
131 }
132 }
133
134 pub fn move_up(&mut self) {
135 let (line, col) = self.cursor_line_col();
136 if line > 0 {
137 let target_line = line - 1;
138 let target_start = self.line_start(target_line);
139 let target_content = self.line_content(target_line);
140 self.cursor = target_start + byte_offset_at_width(target_content, col);
141 }
142 }
143
144 pub fn move_down(&mut self) {
145 let (line, col) = self.cursor_line_col();
146 if line + 1 < self.line_count() {
147 let target_line = line + 1;
148 let target_start = self.line_start(target_line);
149 let target_content = self.line_content(target_line);
150 self.cursor = target_start + byte_offset_at_width(target_content, col);
151 }
152 }
153
154 pub fn move_home(&mut self) {
156 let (line, _) = self.cursor_line_col();
157 self.cursor = self.line_start(line);
158 self.scroll_offset = 0;
159 }
160
161 pub fn move_end(&mut self) {
163 let (line, _) = self.cursor_line_col();
164 self.cursor = self.line_end(line);
165 }
166
167 pub fn update_scroll(&mut self, visible_width: usize) {
169 let (_, col) = self.cursor_line_col();
170 if col < self.scroll_offset {
171 self.scroll_offset = col;
172 } else if col >= self.scroll_offset + visible_width {
173 self.scroll_offset = col - visible_width + 1;
174 }
175 }
176
177 pub fn update_vertical_scroll(&mut self, visible_height: usize) {
179 let (line, _) = self.cursor_line_col();
180 if line < self.vertical_scroll {
181 self.vertical_scroll = line;
182 } else if line >= self.vertical_scroll + visible_height {
183 self.vertical_scroll = line - visible_height + 1;
184 }
185 }
186
187 fn prev_char_boundary(&self) -> usize {
188 let mut pos = self.cursor - 1;
189 while !self.content.is_char_boundary(pos) {
190 pos -= 1;
191 }
192 pos
193 }
194
195 fn next_char_boundary(&self) -> usize {
196 let mut pos = self.cursor + 1;
197 while pos < self.content.len() && !self.content.is_char_boundary(pos) {
198 pos += 1;
199 }
200 pos
201 }
202}
203
204fn byte_offset_at_width(line: &str, target_width: usize) -> usize {
206 let mut width = 0;
207 for (i, c) in line.char_indices() {
208 if width >= target_width {
209 return i;
210 }
211 width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
212 }
213 line.len()
214}
215
216impl Default for Editor {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_insert_and_content() {
228 let mut editor = Editor::new();
229 editor.insert_char('h');
230 editor.insert_char('i');
231 assert_eq!(editor.content(), "hi");
232 assert_eq!(editor.cursor(), 2);
233 }
234
235 #[test]
236 fn test_delete_back() {
237 let mut editor = Editor::with_content("hello".to_string());
238 editor.delete_back();
239 assert_eq!(editor.content(), "hell");
240 }
241
242 #[test]
243 fn test_cursor_movement() {
244 let mut editor = Editor::with_content("hello".to_string());
245 editor.move_left();
246 assert_eq!(editor.cursor(), 4);
247 editor.move_home();
248 assert_eq!(editor.cursor(), 0);
249 editor.move_end();
250 assert_eq!(editor.cursor(), 5);
251 }
252
253 #[test]
254 fn test_insert_newline() {
255 let mut editor = Editor::new();
256 editor.insert_char('a');
257 editor.insert_newline();
258 editor.insert_char('b');
259 assert_eq!(editor.content(), "a\nb");
260 assert_eq!(editor.cursor(), 3);
261 }
262
263 #[test]
264 fn test_cursor_line_col() {
265 let editor = Editor::with_content("abc\ndef\nghi".to_string());
266 assert_eq!(editor.cursor_line_col(), (2, 3));
268 }
269
270 #[test]
271 fn test_move_up_down() {
272 let mut editor = Editor::with_content("abc\ndef\nghi".to_string());
273 editor.move_up();
275 assert_eq!(editor.cursor_line_col(), (1, 3));
276 assert_eq!(&editor.content()[..editor.cursor()], "abc\ndef");
277 editor.move_up();
278 assert_eq!(editor.cursor_line_col(), (0, 3));
279 assert_eq!(&editor.content()[..editor.cursor()], "abc");
280 editor.move_up();
282 assert_eq!(editor.cursor_line_col(), (0, 3));
283 editor.move_down();
285 assert_eq!(editor.cursor_line_col(), (1, 3));
286 }
287
288 #[test]
289 fn test_move_up_clamps_column() {
290 let mut editor = Editor::with_content("abcdef\nab\nxyz".to_string());
291 editor.move_up();
293 assert_eq!(editor.cursor_line_col(), (1, 2));
295 editor.move_up();
296 assert_eq!(editor.cursor_line_col(), (0, 2));
298 }
299
300 #[test]
301 fn test_line_helpers() {
302 let editor = Editor::with_content("abc\ndef\nghi".to_string());
303 assert_eq!(editor.line_count(), 3);
304 assert_eq!(editor.line_content(0), "abc");
305 assert_eq!(editor.line_content(1), "def");
306 assert_eq!(editor.line_content(2), "ghi");
307 }
308
309 #[test]
310 fn test_home_end_multiline() {
311 let mut editor = Editor::with_content("abc\ndef".to_string());
312 editor.move_home();
314 assert_eq!(editor.cursor(), 4); assert_eq!(editor.cursor_line_col(), (1, 0));
317 editor.move_end();
318 assert_eq!(editor.cursor(), 7); assert_eq!(editor.cursor_line_col(), (1, 3));
320 }
321
322 #[test]
323 fn test_vertical_scroll() {
324 let mut editor = Editor::with_content("a\nb\nc\nd\ne".to_string());
325 editor.update_vertical_scroll(3);
326 assert_eq!(editor.vertical_scroll(), 2);
328 }
329
330 #[test]
331 fn test_delete_back_across_newline() {
332 let mut editor = Editor::with_content("abc\ndef".to_string());
333 editor.cursor = 4;
335 editor.delete_back();
336 assert_eq!(editor.content(), "abcdef");
337 assert_eq!(editor.cursor(), 3);
338 }
339}