Skip to main content

vimltui/editor/
mod.rs

1pub mod input;
2pub mod motions;
3pub mod operators;
4pub mod search;
5pub mod visual;
6
7use crate::{
8    EditRecord, FindDirection, Operator, Register, SearchState, Snapshot, VimMode, VimModeConfig,
9    YankHighlight, SCROLLOFF,
10};
11
12/// A self-contained Vim editor instance with its own buffer, cursor, mode, and state.
13/// Each view that needs a Vim editor creates its own VimEditor.
14pub struct VimEditor {
15    pub lines: Vec<String>,
16    pub cursor_row: usize,
17    pub cursor_col: usize,
18    pub mode: VimMode,
19    pub config: VimModeConfig,
20
21    // Scroll
22    pub scroll_offset: usize,
23    pub visible_height: usize,
24
25    // Undo/Redo
26    pub undo_stack: Vec<Snapshot>,
27    pub redo_stack: Vec<Snapshot>,
28
29    // Registers
30    pub unnamed_register: Register,
31
32    // Search
33    pub search: SearchState,
34
35    // Visual mode anchor
36    pub visual_anchor: Option<(usize, usize)>,
37
38    // Pending operator/count for Normal mode commands
39    pub pending_count: Option<usize>,
40    pub pending_operator: Option<Operator>,
41    pub pending_g: bool,
42    pub pending_register: bool, // waiting for register name after "
43    pub use_system_clipboard: bool, // next yank/paste uses system clipboard
44    pub pending_find: Option<(FindDirection, bool)>, // for f/F/t/T (direction, before_flag)
45    pub last_find: Option<(FindDirection, bool, char)>, // for ;/, repeat
46    pub pending_replace: bool, // for r command
47    pub pending_z: bool, // for z-prefix (zz, zt, zb)
48    pub pending_text_object: Option<bool>, // Some(false)=inner, Some(true)=around
49
50    // Repeat
51    pub last_edit: Option<EditRecord>,
52    pub recording_edit: Vec<crossterm::event::KeyEvent>,
53    pub is_recording: bool,
54
55    // Yank highlight
56    pub yank_highlight: Option<YankHighlight>,
57
58    // Status
59    pub modified: bool,
60    pub command_line: String,
61
62    // Command mode (:)
63    pub command_active: bool,
64    pub command_buffer: String,
65
66    // Live substitution preview
67    pub preview_lines: Option<Vec<String>>,
68    /// Highlight ranges for replacement text in preview: (row, start_col, end_col)
69    pub preview_highlights: Vec<(usize, usize, usize)>,
70}
71
72impl VimEditor {
73    pub fn new(content: &str, config: VimModeConfig) -> Self {
74        let expanded = content.replace('\t', "    ");
75        let lines: Vec<String> = if expanded.is_empty() {
76            vec![String::new()]
77        } else {
78            expanded.lines().map(String::from).collect()
79        };
80
81        Self {
82            lines,
83            cursor_row: 0,
84            cursor_col: 0,
85            mode: VimMode::Normal,
86            config,
87            scroll_offset: 0,
88            visible_height: 20,
89            undo_stack: Vec::new(),
90            redo_stack: Vec::new(),
91            unnamed_register: Register::default(),
92            search: SearchState::default(),
93            visual_anchor: None,
94            pending_count: None,
95            pending_operator: None,
96            pending_g: false,
97            pending_register: false,
98            use_system_clipboard: false,
99            pending_find: None,
100            last_find: None,
101            pending_replace: false,
102            pending_z: false,
103            pending_text_object: None,
104            last_edit: None,
105            recording_edit: Vec::new(),
106            is_recording: false,
107            yank_highlight: None,
108            modified: false,
109            command_line: String::new(),
110            command_active: false,
111            command_buffer: String::new(),
112            preview_lines: None,
113            preview_highlights: Vec::new(),
114        }
115    }
116
117    pub fn new_empty(config: VimModeConfig) -> Self {
118        Self::new("", config)
119    }
120
121    pub fn set_content(&mut self, content: &str) {
122        let expanded = content.replace('\t', "    ");
123        self.lines = if expanded.is_empty() {
124            vec![String::new()]
125        } else {
126            expanded.lines().map(String::from).collect()
127        };
128        self.cursor_row = 0;
129        self.cursor_col = 0;
130        self.scroll_offset = 0;
131        self.undo_stack.clear();
132        self.redo_stack.clear();
133        self.modified = false;
134    }
135
136    pub fn content(&self) -> String {
137        self.lines.join("\n")
138    }
139
140    /// Get the visually selected text
141    pub fn selected_text(&self) -> Option<String> {
142        let ((sr, sc), (er, ec)) = self.visual_range()?;
143        let kind = match &self.mode {
144            super::VimMode::Visual(k) => k.clone(),
145            _ => return None,
146        };
147
148        match kind {
149            super::VisualKind::Line => {
150                Some(self.lines[sr..=er].join("\n"))
151            }
152            super::VisualKind::Char => {
153                if sr == er {
154                    let line = &self.lines[sr];
155                    let s = sc.min(line.len());
156                    let e = (ec + 1).min(line.len());
157                    Some(line[s..e].to_string())
158                } else {
159                    let mut text = String::new();
160                    let first = &self.lines[sr];
161                    text.push_str(&first[sc.min(first.len())..]);
162                    for row in (sr + 1)..er {
163                        text.push('\n');
164                        text.push_str(&self.lines[row]);
165                    }
166                    text.push('\n');
167                    let last = &self.lines[er];
168                    text.push_str(&last[..(ec + 1).min(last.len())]);
169                    Some(text)
170                }
171            }
172            super::VisualKind::Block => {
173                let left = sc.min(ec);
174                let right = sc.max(ec) + 1;
175                let mut text = String::new();
176                for row in sr..=er {
177                    let line = &self.lines[row];
178                    let s = left.min(line.len());
179                    let e = right.min(line.len());
180                    if !text.is_empty() {
181                        text.push('\n');
182                    }
183                    text.push_str(&line[s..e]);
184                }
185                Some(text)
186            }
187        }
188    }
189
190    #[allow(dead_code)]
191    pub fn line_count(&self) -> usize {
192        self.lines.len()
193    }
194
195    /// Returns the cursor shape hint for the current mode.
196    pub fn cursor_shape(&self) -> crate::CursorShape {
197        if self.pending_replace {
198            return crate::CursorShape::Underline;
199        }
200        match &self.mode {
201            VimMode::Normal => crate::CursorShape::Block,
202            VimMode::Insert => crate::CursorShape::Bar,
203            VimMode::Replace => crate::CursorShape::Underline,
204            VimMode::Visual(_) => crate::CursorShape::Block,
205        }
206    }
207
208    pub fn current_line(&self) -> &str {
209        self.lines.get(self.cursor_row).map(|s| s.as_str()).unwrap_or("")
210    }
211
212    pub fn current_line_len(&self) -> usize {
213        self.current_line().len()
214    }
215
216    /// Clamp cursor column to valid range for current line
217    pub fn clamp_cursor(&mut self) {
218        let max_col = match self.mode {
219            VimMode::Insert | VimMode::Replace => self.current_line_len(),
220            _ => self.current_line_len().saturating_sub(1).max(0),
221        };
222        if self.cursor_col > max_col {
223            self.cursor_col = max_col;
224        }
225        if self.cursor_row >= self.lines.len() {
226            self.cursor_row = self.lines.len().saturating_sub(1);
227        }
228    }
229
230    /// Save current state for undo
231    pub fn save_undo(&mut self) {
232        self.undo_stack.push(Snapshot {
233            lines: self.lines.clone(),
234            cursor_row: self.cursor_row,
235            cursor_col: self.cursor_col,
236        });
237        self.redo_stack.clear();
238    }
239
240    /// Undo last change
241    pub fn undo(&mut self) {
242        if let Some(snapshot) = self.undo_stack.pop() {
243            self.redo_stack.push(Snapshot {
244                lines: self.lines.clone(),
245                cursor_row: self.cursor_row,
246                cursor_col: self.cursor_col,
247            });
248            self.lines = snapshot.lines;
249            self.cursor_row = snapshot.cursor_row;
250            self.cursor_col = snapshot.cursor_col;
251            self.clamp_cursor();
252            self.modified = true;
253        }
254    }
255
256    /// Redo last undone change
257    pub fn redo(&mut self) {
258        if let Some(snapshot) = self.redo_stack.pop() {
259            self.undo_stack.push(Snapshot {
260                lines: self.lines.clone(),
261                cursor_row: self.cursor_row,
262                cursor_col: self.cursor_col,
263            });
264            self.lines = snapshot.lines;
265            self.cursor_row = snapshot.cursor_row;
266            self.cursor_col = snapshot.cursor_col;
267            self.clamp_cursor();
268            self.modified = true;
269        }
270    }
271
272    /// Ensure scroll keeps cursor visible with scrolloff
273    pub fn ensure_cursor_visible(&mut self) {
274        let scrolloff = SCROLLOFF.min(self.visible_height / 2);
275
276        if self.cursor_row < self.scroll_offset + scrolloff {
277            self.scroll_offset = self.cursor_row.saturating_sub(scrolloff);
278        }
279
280        if self.cursor_row + scrolloff >= self.scroll_offset + self.visible_height {
281            self.scroll_offset = (self.cursor_row + scrolloff + 1).saturating_sub(self.visible_height);
282        }
283
284        let max_offset = self.lines.len().saturating_sub(self.visible_height);
285        if self.scroll_offset > max_offset {
286            self.scroll_offset = max_offset;
287        }
288    }
289
290    // --- Insert mode text operations ---
291
292    pub fn insert_char(&mut self, c: char) {
293        if self.cursor_row < self.lines.len() {
294            let col = self.cursor_col.min(self.lines[self.cursor_row].len());
295            self.lines[self.cursor_row].insert(col, c);
296            self.cursor_col = col + 1;
297            self.modified = true;
298        }
299    }
300
301    pub fn insert_newline(&mut self) {
302        if self.cursor_row < self.lines.len() {
303            let col = self.cursor_col.min(self.lines[self.cursor_row].len());
304            let indent = {
305                let line = &self.lines[self.cursor_row];
306                let trimmed = line.trim_start();
307                line[..line.len() - trimmed.len()].to_string()
308            };
309            let rest = self.lines[self.cursor_row][col..].to_string();
310            self.lines[self.cursor_row].truncate(col);
311            self.cursor_row += 1;
312            self.lines
313                .insert(self.cursor_row, format!("{}{}", indent, rest));
314            self.cursor_col = indent.len();
315            self.modified = true;
316        }
317    }
318
319    pub fn backspace(&mut self) {
320        if self.cursor_col > 0 {
321            let col = self.cursor_col.min(self.lines[self.cursor_row].len());
322            if col > 0 {
323                self.lines[self.cursor_row].remove(col - 1);
324                self.cursor_col = col - 1;
325                self.modified = true;
326            }
327        } else if self.cursor_row > 0 {
328            let current_line = self.lines.remove(self.cursor_row);
329            self.cursor_row -= 1;
330            self.cursor_col = self.lines[self.cursor_row].len();
331            self.lines[self.cursor_row].push_str(&current_line);
332            self.modified = true;
333        }
334    }
335
336    // --- Delete operations ---
337
338    pub fn delete_char_at_cursor(&mut self) {
339        if self.cursor_row < self.lines.len() {
340            let line_len = self.lines[self.cursor_row].len();
341            if self.cursor_col < line_len {
342                let ch = self.lines[self.cursor_row].remove(self.cursor_col);
343                self.unnamed_register = Register {
344                    content: ch.to_string(),
345                    linewise: false,
346                };
347                self.modified = true;
348                self.clamp_cursor();
349            }
350        }
351    }
352
353    #[allow(dead_code)]
354    pub fn delete_line(&mut self, row: usize) -> Option<String> {
355        if row < self.lines.len() {
356            let line = self.lines.remove(row);
357            if self.lines.is_empty() {
358                self.lines.push(String::new());
359            }
360            self.clamp_cursor();
361            self.modified = true;
362            Some(line)
363        } else {
364            None
365        }
366    }
367
368    pub fn delete_lines(&mut self, start: usize, count: usize) -> String {
369        let end = (start + count).min(self.lines.len());
370        let removed: Vec<String> = self.lines.drain(start..end).collect();
371        if self.lines.is_empty() {
372            self.lines.push(String::new());
373        }
374        if self.cursor_row >= self.lines.len() {
375            self.cursor_row = self.lines.len() - 1;
376        }
377        self.clamp_cursor();
378        self.modified = true;
379        removed.join("\n")
380    }
381
382    pub fn delete_range(&mut self, start_col: usize, end_col: usize, row: usize) -> String {
383        if row >= self.lines.len() {
384            return String::new();
385        }
386        let line_len = self.lines[row].len();
387        let s = start_col.min(line_len);
388        let e = end_col.min(line_len);
389        if s >= e {
390            return String::new();
391        }
392        let removed: String = self.lines[row][s..e].to_string();
393        self.lines[row] = format!("{}{}", &self.lines[row][..s], &self.lines[row][e..]);
394        self.modified = true;
395        removed
396    }
397
398    // --- Paste ---
399
400    #[allow(dead_code)]
401    pub fn paste_after(&mut self) {
402        let reg = self.unnamed_register.clone();
403        if reg.content.is_empty() {
404            return;
405        }
406        self.save_undo();
407        if reg.linewise {
408            let new_lines: Vec<String> = reg.content.lines().map(String::from).collect();
409            let insert_at = (self.cursor_row + 1).min(self.lines.len());
410            for (i, line) in new_lines.into_iter().enumerate() {
411                self.lines.insert(insert_at + i, line);
412            }
413            self.cursor_row = insert_at;
414            self.cursor_col = 0;
415        } else {
416            let col = (self.cursor_col + 1).min(self.lines[self.cursor_row].len());
417            self.lines[self.cursor_row].insert_str(col, &reg.content);
418            self.cursor_col = col + reg.content.len() - 1;
419        }
420        self.modified = true;
421    }
422
423    #[allow(dead_code)]
424    pub fn paste_before(&mut self) {
425        let reg = self.unnamed_register.clone();
426        if reg.content.is_empty() {
427            return;
428        }
429        self.save_undo();
430        if reg.linewise {
431            let new_lines: Vec<String> = reg.content.lines().map(String::from).collect();
432            for (i, line) in new_lines.into_iter().enumerate() {
433                self.lines.insert(self.cursor_row + i, line);
434            }
435            self.cursor_col = 0;
436        } else {
437            let col = self.cursor_col.min(self.lines[self.cursor_row].len());
438            self.lines[self.cursor_row].insert_str(col, &reg.content);
439            self.cursor_col = col + reg.content.len() - 1;
440        }
441        self.modified = true;
442    }
443
444    // --- Join lines ---
445
446    pub fn join_lines(&mut self) {
447        if self.cursor_row + 1 < self.lines.len() {
448            self.save_undo();
449            let next_line = self.lines.remove(self.cursor_row + 1);
450            let trimmed = next_line.trim_start();
451            let join_col = self.lines[self.cursor_row].len();
452            if !self.lines[self.cursor_row].is_empty() && !trimmed.is_empty() {
453                self.lines[self.cursor_row].push(' ');
454                self.cursor_col = join_col;
455            } else {
456                self.cursor_col = join_col;
457            }
458            self.lines[self.cursor_row].push_str(trimmed);
459            self.modified = true;
460        }
461    }
462
463    // --- Indentation ---
464
465    pub fn indent_line(&mut self, row: usize) {
466        if row < self.lines.len() {
467            self.lines[row].insert_str(0, "    ");
468            self.modified = true;
469        }
470    }
471
472    pub fn dedent_line(&mut self, row: usize) {
473        if row < self.lines.len() {
474            let line = &self.lines[row];
475            let spaces = line.len() - line.trim_start().len();
476            let remove = spaces.min(4);
477            if remove > 0 {
478                self.lines[row] = self.lines[row][remove..].to_string();
479                self.modified = true;
480            }
481        }
482    }
483
484    /// Get effective count: pending_count or 1
485    pub fn take_count(&mut self) -> usize {
486        self.pending_count.take().unwrap_or(1)
487    }
488
489    /// Update command line based on current mode
490    pub fn update_command_line(&mut self) {
491        if self.command_active {
492            self.command_line = format!(":{}", self.command_buffer);
493            // Live preview: highlight substitution pattern + replacement
494            self.search.pattern = self
495                .extract_substitute_pattern()
496                .unwrap_or_default();
497            if let Some((lines, hl)) = self.compute_substitute_preview() {
498                self.preview_lines = Some(lines);
499                self.preview_highlights = hl;
500            } else {
501                self.preview_lines = None;
502                self.preview_highlights.clear();
503            }
504            return;
505        }
506        self.command_line = match &self.mode {
507            VimMode::Normal => {
508                if self.search.active {
509                    let prefix = if self.search.forward { "/" } else { "?" };
510                    format!("{}{}", prefix, self.search.input_buffer)
511                } else if self.pending_operator.is_some() || self.pending_count.is_some() {
512                    let mut s = String::new();
513                    if let Some(n) = self.pending_count {
514                        s.push_str(&n.to_string());
515                    }
516                    if let Some(op) = &self.pending_operator {
517                        s.push(match op {
518                            Operator::Delete => 'd',
519                            Operator::Yank => 'y',
520                            Operator::Change => 'c',
521                            Operator::Indent => '>',
522                            Operator::Dedent => '<',
523                            Operator::Uppercase => 'U',
524                            Operator::Lowercase => 'u',
525                            Operator::ToggleCase => '~',
526                        });
527                    }
528                    s
529                } else {
530                    String::new()
531                }
532            }
533            VimMode::Insert => "-- INSERT --".to_string(),
534            VimMode::Replace => "-- REPLACE --".to_string(),
535            VimMode::Visual(kind) => {
536                let label = match kind {
537                    super::VisualKind::Char => "VISUAL",
538                    super::VisualKind::Line => "VISUAL LINE",
539                    super::VisualKind::Block => "VISUAL BLOCK",
540                };
541                format!("-- {} --", label)
542            }
543        };
544    }
545
546    /// Extract the search pattern from a partial substitution command in command_buffer.
547    fn extract_substitute_pattern(&self) -> Option<String> {
548        let (pattern, _, _, _) = self.extract_substitute_parts()?;
549        Some(pattern)
550    }
551
552    /// Parse partial substitution command, returning (pattern, replacement, range_all, flags).
553    /// Handles incomplete input gracefully (e.g., `:s/hol` without closing delimiter).
554    fn extract_substitute_parts(&self) -> Option<(String, Option<String>, bool, String)> {
555        let cmd = self.command_buffer.trim();
556
557        // Strip range prefix and determine if % (all lines)
558        let (all, rest) = if cmd.starts_with('%') {
559            (true, &cmd[1..])
560        } else if let Some(pos) = cmd.find('s') {
561            let prefix = &cmd[..pos];
562            if prefix.is_empty() || prefix.chars().all(|c| c.is_ascii_digit() || c == ',') {
563                (false, &cmd[pos..])
564            } else {
565                return None;
566            }
567        } else {
568            return None;
569        };
570
571        if !rest.starts_with('s') || rest.len() < 3 {
572            return None;
573        }
574
575        let delim = rest.as_bytes()[1] as char;
576        if delim.is_alphanumeric() {
577            return None;
578        }
579
580        // Parse: s/pattern/replacement/flags — each part may be incomplete
581        let body = &rest[2..];
582        let mut parts: Vec<String> = Vec::new();
583        let mut current = String::new();
584        let mut chars = body.chars().peekable();
585        while let Some(c) = chars.next() {
586            if c == '\\' {
587                if let Some(&next) = chars.peek() {
588                    if next == delim {
589                        current.push(next);
590                        chars.next();
591                        continue;
592                    }
593                }
594                current.push(c);
595            } else if c == delim {
596                parts.push(current.clone());
597                current.clear();
598            } else {
599                current.push(c);
600            }
601        }
602
603        let pattern = if let Some(p) = parts.first() {
604            if p.is_empty() { return None; }
605            p.clone()
606        } else if !current.is_empty() {
607            // Still typing the pattern (no closing delimiter yet)
608            return Some((current, None, all, String::new()));
609        } else {
610            return None;
611        };
612
613        let replacement = if parts.len() >= 2 {
614            Some(parts[1].clone())
615        } else if !current.is_empty() {
616            // Still typing the replacement
617            Some(current.clone())
618        } else {
619            // Just closed the pattern delimiter, replacement is empty so far
620            Some(String::new())
621        };
622
623        let flags = if parts.len() >= 3 {
624            parts[2].clone()
625        } else if parts.len() >= 2 {
626            current
627        } else {
628            String::new()
629        };
630
631        Some((pattern, replacement, all, flags))
632    }
633
634    /// Determine if a pattern should be case-insensitive (smartcase):
635    /// all-lowercase → insensitive, any uppercase → sensitive.
636    /// The `i` flag forces insensitive regardless.
637    fn is_smartcase_insensitive(pattern: &str, flags: &str) -> bool {
638        if flags.contains('i') {
639            return true;
640        }
641        // Smartcase: if pattern has no uppercase letters, match case-insensitively
642        !pattern.chars().any(|c| c.is_uppercase())
643    }
644
645    /// Compute preview lines and highlight ranges for replacement text.
646    fn compute_substitute_preview(
647        &self,
648    ) -> Option<(Vec<String>, Vec<(usize, usize, usize)>)> {
649        let (pattern, replacement, all, flags) = self.extract_substitute_parts()?;
650        let replacement = replacement?;
651
652        let case_insensitive = Self::is_smartcase_insensitive(&pattern, &flags);
653        let global = flags.contains('g');
654
655        let regex_pattern = if case_insensitive {
656            format!("(?i){}", pattern)
657        } else {
658            pattern
659        };
660        let re = regex::Regex::new(&regex_pattern).ok()?;
661
662        let (start, end) = if all {
663            (0, self.lines.len().saturating_sub(1))
664        } else {
665            (self.cursor_row, self.cursor_row)
666        };
667
668        let mut preview = self.lines.clone();
669        let mut highlights = Vec::new();
670
671        for row in start..=end.min(preview.len().saturating_sub(1)) {
672            let line = &self.lines[row];
673            // Build new line and track replacement positions
674            let mut new_line = String::new();
675            let mut last_end = 0;
676            let matches: Vec<_> = re.find_iter(line).collect();
677            let match_count = if global { matches.len() } else { matches.len().min(1) };
678
679            for m in matches.iter().take(match_count) {
680                new_line.push_str(&line[last_end..m.start()]);
681                let rep_start = new_line.len();
682                // Expand replacement (handles $1, $2 etc.)
683                let expanded = re.replace(m.as_str(), replacement.as_str());
684                new_line.push_str(&expanded);
685                let rep_end = new_line.len();
686                if rep_start < rep_end {
687                    highlights.push((row, rep_start, rep_end));
688                }
689                last_end = m.end();
690            }
691            new_line.push_str(&line[last_end..]);
692            preview[row] = new_line;
693        }
694
695        Some((preview, highlights))
696    }
697
698    // --- System clipboard ---
699
700    pub fn copy_to_system_clipboard(&self, text: &str) {
701        // Try xclip first, then xsel, then wl-copy (Wayland)
702        let cmds: &[(&str, &[&str])] = &[
703            ("wl-copy", &[]),
704            ("xclip", &["-selection", "clipboard"]),
705            ("xsel", &["--clipboard", "--input"]),
706        ];
707        for (cmd, args) in cmds {
708            if let Ok(mut child) = std::process::Command::new(cmd)
709                .args(*args)
710                .stdin(std::process::Stdio::piped())
711                .stdout(std::process::Stdio::null())
712                .stderr(std::process::Stdio::null())
713                .spawn()
714            {
715                if let Some(mut stdin) = child.stdin.take() {
716                    use std::io::Write;
717                    let _ = stdin.write_all(text.as_bytes());
718                }
719                let _ = child.wait();
720                return;
721            }
722        }
723    }
724
725    pub fn paste_from_system_clipboard(&mut self) {
726        let cmds: &[(&str, &[&str])] = &[
727            ("wl-paste", &["--no-newline"]),
728            ("xclip", &["-selection", "clipboard", "-o"]),
729            ("xsel", &["--clipboard", "--output"]),
730        ];
731        for (cmd, args) in cmds {
732            if let Ok(output) = std::process::Command::new(cmd)
733                .args(*args)
734                .stdout(std::process::Stdio::piped())
735                .stderr(std::process::Stdio::null())
736                .output()
737                && output.status.success() {
738                    if let Ok(text) = String::from_utf8(output.stdout)
739                        && !text.is_empty() {
740                            self.save_undo();
741                            let col = (self.cursor_col + 1).min(self.current_line_len());
742                            // Insert text at cursor
743                            if text.contains('\n') {
744                                let parts: Vec<&str> = text.split('\n').collect();
745                                let after = self.lines[self.cursor_row][col..].to_string();
746                                self.lines[self.cursor_row].truncate(col);
747                                self.lines[self.cursor_row].push_str(parts[0]);
748                                for (i, part) in parts[1..].iter().enumerate() {
749                                    self.lines.insert(self.cursor_row + 1 + i, part.to_string());
750                                }
751                                let last_row = self.cursor_row + parts.len() - 1;
752                                self.lines[last_row].push_str(&after);
753                                self.cursor_row = last_row;
754                                self.cursor_col = self.lines[last_row].len() - after.len();
755                            } else {
756                                self.lines[self.cursor_row].insert_str(col, &text);
757                                self.cursor_col = col + text.len() - 1;
758                            }
759                            self.modified = true;
760                        }
761                    return;
762                }
763        }
764    }
765}