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
12pub 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 pub scroll_offset: usize,
23 pub visible_height: usize,
24
25 pub undo_stack: Vec<Snapshot>,
27 pub redo_stack: Vec<Snapshot>,
28
29 pub unnamed_register: Register,
31
32 pub search: SearchState,
34
35 pub visual_anchor: Option<(usize, usize)>,
37
38 pub pending_count: Option<usize>,
40 pub pending_operator: Option<Operator>,
41 pub pending_g: bool,
42 pub pending_register: bool, pub use_system_clipboard: bool, pub pending_find: Option<(FindDirection, bool)>, pub last_find: Option<(FindDirection, bool, char)>, pub pending_replace: bool, pub pending_z: bool, pub pending_text_object: Option<bool>, pub last_edit: Option<EditRecord>,
52 pub recording_edit: Vec<crossterm::event::KeyEvent>,
53 pub is_recording: bool,
54
55 pub yank_highlight: Option<YankHighlight>,
57
58 pub modified: bool,
60 pub command_line: String,
61
62 pub command_active: bool,
64 pub command_buffer: String,
65
66 pub preview_lines: Option<Vec<String>>,
68 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 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 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 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 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 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 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 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 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(¤t_line);
332 self.modified = true;
333 }
334 }
335
336 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 #[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, ®.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, ®.content);
439 self.cursor_col = col + reg.content.len() - 1;
440 }
441 self.modified = true;
442 }
443
444 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 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 pub fn take_count(&mut self) -> usize {
486 self.pending_count.take().unwrap_or(1)
487 }
488
489 pub fn update_command_line(&mut self) {
491 if self.command_active {
492 self.command_line = format!(":{}", self.command_buffer);
493 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 fn extract_substitute_pattern(&self) -> Option<String> {
548 let (pattern, _, _, _) = self.extract_substitute_parts()?;
549 Some(pattern)
550 }
551
552 fn extract_substitute_parts(&self) -> Option<(String, Option<String>, bool, String)> {
555 let cmd = self.command_buffer.trim();
556
557 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 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 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 Some(current.clone())
618 } else {
619 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 fn is_smartcase_insensitive(pattern: &str, flags: &str) -> bool {
638 if flags.contains('i') {
639 return true;
640 }
641 !pattern.chars().any(|c| c.is_uppercase())
643 }
644
645 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(®ex_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 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 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 pub fn copy_to_system_clipboard(&self, text: &str) {
701 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 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}