moros/usr/
edit.rs

1use crate::api;
2use crate::api::console::Style;
3use crate::api::process::ExitCode;
4use crate::api::prompt::Prompt;
5use crate::api::regex::Regex;
6use crate::api::{console, fs, io};
7
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec;
11use alloc::vec::Vec;
12use core::cmp;
13
14enum Cmd {
15    Delete,
16    Open,
17    Quit,
18    Replace,
19    Save,
20}
21
22struct EditorConfig {
23    tab_size: usize,
24}
25
26#[derive(Clone)]
27struct Coords {
28    pub x: usize,
29    pub y: usize,
30}
31
32#[derive(Clone)]
33pub struct Buffer {
34    pathname: String,
35    lines: Vec<String>,
36    cursor: Coords,
37    offset: Coords,
38    highlighted: Vec<(usize, usize, char)>,
39}
40
41impl From<&str> for Buffer {
42    fn from(pathname: &str) -> Self {
43        let p: Vec<&str> = pathname.split(':').collect();
44        let pathname = p[0].to_string();
45        let y = p.get(1).and_then(|s| {
46            s.parse::<usize>().ok()
47        }).unwrap_or(1).saturating_sub(1);
48        let x = p.get(2).and_then(|s| {
49            s.parse::<usize>().ok()
50        }).unwrap_or(1).saturating_sub(1);
51
52        let cursor = Coords { x: x % cols(), y: y % rows() };
53        let offset = Coords { x: x - cursor.x, y: y - cursor.y };
54        let highlighted = Vec::new();
55        let mut lines = Vec::new();
56
57        match fs::read_to_string(&pathname) {
58            Ok(contents) => {
59                for line in contents.lines() {
60                    lines.push(line.into());
61                }
62                if lines.is_empty() {
63                    lines.push(String::new());
64                }
65            }
66            Err(_) => {
67                lines.push(String::new());
68            }
69        };
70
71        Self {
72            pathname,
73            lines,
74            cursor,
75            offset,
76            highlighted,
77        }
78    }
79}
80
81impl From<&Editor> for Buffer {
82    fn from(editor: &Editor) -> Self {
83        Buffer {
84            pathname: editor.pathname.clone(),
85            lines: editor.lines.clone(),
86            cursor: editor.cursor.clone(),
87            offset: editor.offset.clone(),
88            highlighted: editor.highlighted.clone(),
89        }
90    }
91}
92
93pub struct Editor {
94    buffer_prompt: Prompt,
95    buffers: Vec<Buffer>,
96    buf: usize,
97
98    pathname: String,
99    lines: Vec<String>,
100    cursor: Coords,
101    offset: Coords,
102    highlighted: Vec<(usize, usize, char)>,
103
104    clipboard: Option<String>,
105    config: EditorConfig,
106    search_prompt: Prompt,
107    search_query: String,
108    command_prompt: Prompt,
109    command_history: String,
110}
111
112impl Editor {
113    pub fn new(pathname: &str) -> Self {
114        let clipboard = None;
115        let config = EditorConfig { tab_size: 4 };
116
117        let search_query = String::new();
118        let mut search_prompt = Prompt::new();
119        search_prompt.eol = false;
120
121        let mut command_prompt = Prompt::new();
122        let command_history = "~/.edit-history".to_string();
123        command_prompt.history.load(&command_history);
124        command_prompt.eol = false;
125
126        // TODO: Add path autocompletion
127        let mut buffer_prompt = Prompt::new();
128        buffer_prompt.eol = false;
129
130        let buf = Buffer::from(pathname);
131
132        let pathname = buf.pathname.clone();
133        let lines = buf.lines.clone();
134        let cursor = buf.cursor.clone();
135        let offset = buf.offset.clone();
136        let highlighted = buf.highlighted.clone();
137
138        let buffers = vec![buf];
139        let buf = 0;
140
141        Self {
142            buffer_prompt,
143            buffers,
144            buf,
145            pathname,
146            clipboard,
147            lines,
148            cursor,
149            offset,
150            highlighted,
151            config,
152            search_prompt,
153            search_query,
154            command_prompt,
155            command_history,
156        }
157    }
158
159    pub fn save(&mut self, path: &str) -> Result<(), ExitCode> {
160        let contents = self.lines.join("\n") + "\n";
161
162        if fs::write(path, contents.as_bytes()).is_ok() {
163            self.pathname = path.into();
164            let n = self.lines.len();
165            let status = format!("Wrote {}L to '{}'", n, path);
166            self.print_status(&status, "yellow");
167            Ok(())
168        } else {
169            let status = format!("Could not write to '{}'", path);
170            self.print_status(&status, "red");
171            Err(ExitCode::Failure)
172        }
173    }
174
175    fn print_status(&mut self, status: &str, background: &str) {
176        // Move cursor to the bottom of the screen
177        print!("\x1b[{};1H", rows() + 1);
178
179        let color = Style::color("black").with_background(background);
180        let reset = Style::reset();
181        print!("{}{:cols$}{}", color, status, reset, cols = cols());
182
183        // Move cursor back
184        print!("\x1b[{};{}H", self.cursor.y + 1, self.cursor.x + 1);
185    }
186
187    fn print_editing_status(&mut self) {
188        let max = 50;
189        let mut path = self.pathname.clone();
190        if self.pathname.chars().count() > max {
191            path.truncate(max - 3);
192            path.push_str("...");
193        }
194        let start = format!("Editing '{}'", path);
195
196        let x = self.offset.x + self.cursor.x + 1;
197        let y = self.offset.y + self.cursor.y + 1;
198        let n = y * 100 / self.lines.len();
199        let end = format!("{},{} {:3}%", y, x, n);
200
201        let width = cols() - start.chars().count();
202        let status = format!("{}{:>width$}", start, end, width = width);
203
204        self.print_status(&status, "silver");
205    }
206
207    fn print_screen(&mut self) {
208        let mut lines: Vec<String> = Vec::new();
209        let a = self.offset.y;
210        let b = self.offset.y + rows();
211        for y in a..b {
212            lines.push(self.render_line(y));
213        }
214        println!("\x1b[1;1H{}", lines.join("\n"));
215    }
216
217    fn render_line(&self, y: usize) -> String {
218        // Render line into a row of the screen, or an empty row when past EOF
219        let line = if y < self.lines.len() {
220            &self.lines[y]
221        } else {
222            ""
223        };
224
225        let s = format!("{:cols$}", line, cols = self.offset.x);
226        let mut row: Vec<char> = s.chars().collect();
227        let n = self.offset.x + cols();
228        let after = if row.len() > n {
229            row.truncate(n - 1);
230            truncated_line_indicator()
231        } else {
232            " ".repeat(n - row.len())
233        };
234        row.extend(after.chars());
235        row[self.offset.x..].iter().collect()
236    }
237
238    fn render_char(&self, c: char) -> Option<String> {
239        match c {
240            '\t' => Some(" ".repeat(self.config.tab_size)),
241            c if console::is_printable(c) => Some(c.to_string()),
242            _ => None,
243        }
244    }
245
246    fn match_chars(&mut self, opening: char, closing: char) {
247        let mut stack = Vec::new();
248        let ox = self.offset.x;
249        let oy = self.offset.y;
250        let cx = self.cursor.x;
251        let cy = self.cursor.y;
252        if let Some(cursor) = self.lines[oy + cy].chars().nth(ox + cx) {
253            if cursor == closing {
254                for (y, line) in self.lines.iter().enumerate() {
255                    for (x, c) in line.chars().enumerate() {
256                        if oy + cy == y && ox + cx == x {
257                            // Cursor position
258                            if let Some((x, y)) = stack.pop() {
259                                self.highlighted.push((cx, cy, closing));
260                                let is_col = ox <= x && x < ox + cols();
261                                let is_row = oy <= y && y < oy + rows();
262                                if is_col && is_row {
263                                    self.highlighted.push(
264                                        (x - ox, y - oy, opening)
265                                    );
266                                }
267                            }
268                            return;
269                        }
270                        if c == opening {
271                            stack.push((x, y));
272                        }
273                        if c == closing {
274                            stack.pop();
275                        }
276                    }
277                    if oy + cy == y {
278                        break;
279                    }
280                }
281            }
282            if cursor == opening {
283                for (y, line) in self.lines.iter().enumerate().skip(oy + cy) {
284                    for (x, c) in line.chars().enumerate() {
285                        if y == oy + cy && x <= ox + cx {
286                            continue; // Skip chars before cursor
287                        }
288                        if c == opening {
289                            stack.push((x, y));
290                        }
291                        if c == closing {
292                            if stack.pop().is_none() {
293                                self.highlighted.push((cx, cy, opening));
294                                let is_col = ox <= x && x < ox + cols();
295                                let is_row = oy <= y && y < oy + rows();
296                                if is_col && is_row {
297                                    self.highlighted.push(
298                                        (x - ox, y - oy, closing)
299                                    );
300                                }
301                                return;
302                            }
303                        }
304                    }
305                }
306            }
307        }
308    }
309
310    fn print_highlighted(&mut self) {
311        self.match_chars('(', ')');
312        self.match_chars('{', '}');
313        self.match_chars('[', ']');
314        let color = Style::color("red");
315        let reset = Style::reset();
316        for (x, y, c) in &self.highlighted {
317            if *x == cols() - 1 {
318                continue;
319            }
320            print!("\x1b[{};{}H", y + 1, x + 1);
321            print!("{}{}{}", color, c, reset);
322        }
323    }
324
325    fn clear_highlighted(&mut self) {
326        let reset = Style::reset();
327        for (x, y, c) in &self.highlighted {
328            if *x == cols() - 1 {
329                continue;
330            }
331            print!("\x1b[{};{}H", y + 1, x + 1);
332            print!("{}{}", reset, c);
333        }
334        self.highlighted.clear();
335    }
336
337    // Align cursor that is past the end of the line, to the end
338    // of the line.
339    //
340    // If the cursor is somewhere on the long line on the second
341    // screen in the following diagram, going down should move
342    // the cursor to the end of the short line and display the
343    // first screen instead of the second screen.
344    //
345    // +----------------------------+----------------------------+
346    // |                            |                            |
347    // | This is a loooooooooooooooo|oooooong line               |
348    // | This is a short line       |          ^                 |
349    // |                     ^      |                            |
350    // +----------------------------+----------------------------+
351    fn align_cursor(&mut self) {
352        let x = self.offset.x + self.cursor.x;
353        let y = self.offset.y + self.cursor.y;
354        let eol = self.lines[y].chars().count();
355        if x > eol {
356            let n = cols();
357            self.offset.x = (eol / n) * n;
358            self.cursor.x = eol % n;
359        }
360    }
361
362    pub fn run(&mut self) -> Result<(), ExitCode> {
363        print!("\x1b[2J\x1b[1;1H"); // Clear screen and move to top
364        self.print_screen();
365        self.print_editing_status();
366        self.print_highlighted();
367        print!("\x1b[{};{}H", self.cursor.y + 1, self.cursor.x + 1);
368
369        let mut escape = false;
370        let mut csi = false;
371        let mut csi_params = String::new();
372        loop {
373            let c = io::stdin().read_char().unwrap_or('\0');
374            print!("\x1b[?25l"); // Disable cursor
375            self.clear_highlighted();
376            print!("\x1b[{};{}H", self.cursor.y + 1, self.cursor.x + 1);
377
378            match c {
379                '\x1B' => { // Esc
380                    escape = true;
381                    continue;
382                }
383                '[' if escape => {
384                    csi = true;
385                    csi_params.clear();
386                    continue;
387                }
388                '\0' => {
389                    continue;
390                }
391                '\x11' | '\x03' => { // Ctrl + Q or Ctrl + C
392                    print!("\x1b[2J\x1b[1;1H"); // Clear screen and move to top
393                    print!("\x1b[?25h"); // Enable cursor
394                    break;
395                }
396                '\x17' => { // Ctrl + W
397                    self.save(&self.pathname.clone()).ok();
398                    print!("\x1b[?25h"); // Enable cursor
399                    continue;
400                }
401                '\x18' => { // Ctrl + X
402                    let res = self.save(&self.pathname.clone());
403                    print!("\x1b[2J\x1b[1;1H"); // Clear screen and move to top
404                    print!("\x1b[?25h"); // Enable cursor
405                    return res;
406                }
407                '\n' => { // Newline
408                    self.handle_newline();
409                }
410                '~' if csi && csi_params == "5" => { // Page Up
411                    self.handle_page_up();
412                }
413                '~' if csi && csi_params == "6" => { // Page Down
414                    self.handle_page_down();
415                }
416                'A' if csi => { // Arrow Up
417                    self.handle_arrow_up();
418                }
419                'B' if csi => { // Arrow Down
420                    self.handle_arrow_down();
421                }
422                'C' if csi => { // Arrow Right
423                    let line = &self.lines[self.offset.y + self.cursor.y];
424                    let x = self.cursor.x + self.offset.x;
425                    let n = line.chars().count();
426                    if line.is_empty() || x >= n {
427                        print!("\x1b[?25h"); // Enable cursor
428                        escape = false;
429                        csi = false;
430                        continue;
431                    } else if self.cursor.x == cols() - 1 {
432                        self.offset.x += cols();
433                        self.cursor.x -= cols() - 1;
434                        self.print_screen();
435                    } else {
436                        self.cursor.x += 1;
437                    }
438                }
439                'D' if csi => { // Arrow Left
440                    if self.cursor.x + self.offset.x == 0 {
441                        print!("\x1b[?25h"); // Enable cursor
442                        escape = false;
443                        csi = false;
444                        continue;
445                    } else if self.cursor.x == 0 {
446                        self.offset.x -= cols();
447                        self.cursor.x += cols() - 1;
448                        self.align_cursor();
449                        self.print_screen();
450                    } else {
451                        self.cursor.x -= 1;
452                    }
453                }
454                'Z' if csi => { // Backtab (Shift + Tab)
455                     // Do nothing
456                }
457                'I' if csi && csi_params == "1;5" => { // Ctrl + Tab
458                    self.next_buffer();
459                    self.print_screen();
460                }
461                'I' if csi && csi_params == "1;6" => { // Ctrl + Shift + Tab
462                    self.previous_buffer();
463                    self.print_screen();
464                }
465                '\x14' => { // Ctrl + T -> Go to top of file
466                    self.cursor.x = 0;
467                    self.cursor.y = 0;
468                    self.offset.x = 0;
469                    self.offset.y = 0;
470                    self.print_screen();
471                }
472                '\x02' => { // Ctrl + B -> Go to bottom of file
473                    self.cursor.x = 0;
474                    self.cursor.y = cmp::min(rows(), self.lines.len()) - 1;
475                    self.offset.x = 0;
476                    self.offset.y = self.lines.len() - 1 - self.cursor.y;
477                    self.print_screen();
478                }
479                '\x01' => { // Ctrl + A -> Go to beginning of line
480                    self.cursor.x = 0;
481                    self.offset.x = 0;
482                    self.print_screen();
483                }
484                '\x05' => { // Ctrl + E -> Go to end of line
485                    let line = &self.lines[self.offset.y + self.cursor.y];
486                    let n = line.chars().count();
487                    let w = cols();
488                    self.cursor.x = n % w;
489                    self.offset.x = w * (n / w);
490                    self.print_screen();
491                }
492                '\x04' => { // Ctrl + D -> Delete (cut) line
493                    self.cut_line();
494                }
495                '\x19' => { // Ctrl + Y -> Yank (copy) line
496                    self.copy_line();
497                }
498                '\x10' => { // Ctrl + P -> Put (paste) line
499                    self.paste_line();
500                }
501                '\x06' => { // Ctrl + F -> Find
502                    self.find();
503                    self.print_screen();
504                }
505                '\x0E' => { // Ctrl + N -> Find next
506                    self.find_next();
507                    self.print_screen();
508                }
509                '\x0F' => { // Ctrl + O -> Open buffer
510                    self.open();
511                    self.print_screen();
512                }
513                '\x0B' => { // Ctrl + X -> Kill buffer
514                    self.kill_buffer();
515                    self.print_screen();
516                }
517                '\x0C' => { // Ctrl + L -> Line mode
518                    match self.exec() {
519                        Some(Cmd::Quit) => {
520                            print!("\x1b[2J"); // Clear screen
521                            print!("\x1b[1;1H"); // Move to top
522                            print!("\x1b[?25h"); // Enable cursor
523                            break;
524                        }
525                        Some(Cmd::Save) => {
526                            print!("\x1b[?25h"); // Enable cursor
527                            continue;
528                        }
529                        Some(_) => {
530                            self.print_screen();
531                        }
532                        None => {
533                        }
534                    }
535                }
536                '\x08' => { // Backspace
537                    let y = self.offset.y + self.cursor.y;
538                    if self.offset.x + self.cursor.x > 0 {
539                        // Remove char from line
540                        let mut row: Vec<_> = self.lines[y].chars().collect();
541                        row.remove(self.offset.x + self.cursor.x - 1);
542                        self.lines[y] = row.into_iter().collect();
543
544                        if self.cursor.x == 0 {
545                            self.offset.x -= cols();
546                            self.cursor.x = cols() - 1;
547                            self.print_screen();
548                        } else {
549                            self.cursor.x -= 1;
550                            let line = self.render_line(y);
551                            print!("\x1b[2K\x1b[1G{}", line);
552                        }
553                    } else {
554                        // Remove newline from previous line
555                        if self.cursor.y == 0 && self.offset.y == 0 {
556                            print!("\x1b[?25h"); // Enable cursor
557                            escape = false;
558                            csi = false;
559                            continue;
560                        }
561
562                        // Move cursor below the end of the previous line
563                        let n = self.lines[y - 1].chars().count();
564                        let w = cols();
565                        self.cursor.x = n % w;
566                        self.offset.x = w * (n / w);
567
568                        // Move line to the end of the previous line
569                        let line = self.lines.remove(y);
570                        self.lines[y - 1].push_str(&line);
571
572                        // Move cursor up to the previous line
573                        if self.cursor.y > 0 {
574                            self.cursor.y -= 1;
575                        } else {
576                            self.offset.y -= 1;
577                        }
578
579                        self.print_screen();
580                    }
581                }
582                '\x7f' => {
583                    // Delete
584                    let y = self.offset.y + self.cursor.y;
585                    let n = self.lines[y].chars().count();
586                    if self.offset.x + self.cursor.x >= n {
587                        // Remove newline from line
588                        if y + 1 < self.lines.len() {
589                            let line = self.lines.remove(y + 1);
590                            self.lines[y].push_str(&line);
591                            self.print_screen();
592                        }
593                    } else {
594                        // Remove char from line
595                        self.lines[y].remove(self.offset.x + self.cursor.x);
596                        let line = self.render_line(y);
597                        print!("\x1b[2K\x1b[1G{}", line);
598                    }
599                }
600                c if csi => {
601                    csi_params.push(c);
602                    continue;
603                }
604                c => {
605                    if let Some(s) = self.render_char(c) {
606                        let y = self.offset.y + self.cursor.y;
607                        let mut row: Vec<_> = self.lines[y].chars().collect();
608                        for c in s.chars() {
609                            row.insert(self.offset.x + self.cursor.x, c);
610                            self.cursor.x += 1;
611                        }
612                        self.lines[y] = row.into_iter().collect();
613                        if self.cursor.x >= cols() {
614                            self.offset.x += cols();
615                            self.cursor.x -= cols();
616                            self.print_screen();
617                        } else {
618                            let line = self.render_line(y);
619                            print!("\x1b[2K\x1b[1G{}", line);
620                        }
621                    }
622                }
623            }
624            self.print_editing_status();
625            self.print_highlighted();
626            print!("\x1b[{};{}H", self.cursor.y + 1, self.cursor.x + 1);
627            print!("\x1b[?25h"); // Enable cursor
628            escape = false;
629            csi = false;
630        }
631        Ok(())
632    }
633
634    fn handle_newline(&mut self) {
635        let x = self.offset.x + self.cursor.x;
636        let y = self.offset.y + self.cursor.y;
637
638        let old_line = self.lines[y].clone();
639        let mut row: Vec<char> = old_line.chars().collect();
640        let new_line = row.split_off(x).into_iter().collect();
641        self.lines[y] = row.into_iter().collect();
642        self.lines.insert(y + 1, new_line);
643        if self.cursor.y == rows() - 1 {
644            self.offset.y += 1;
645        } else {
646            self.cursor.y += 1;
647        }
648        self.cursor.x = 0;
649        self.offset.x = 0;
650        self.print_screen();
651    }
652
653    fn handle_page_up(&mut self) {
654        let scroll = rows() - 1; // Keep one line on screen
655        self.offset.y -= cmp::min(scroll, self.offset.y);
656        self.align_cursor();
657        self.print_screen();
658    }
659
660    fn handle_page_down(&mut self) {
661        let scroll = rows() - 1; // Keep one line on screen
662        let n = cmp::max(self.lines.len(), 1);
663        let remaining = n - self.offset.y - 1;
664        self.offset.y += cmp::min(scroll, remaining);
665        if self.cursor.y + scroll > remaining {
666            self.cursor.y = 0;
667        }
668        self.align_cursor();
669        self.print_screen();
670    }
671
672    fn handle_arrow_up(&mut self) {
673        if self.cursor.y > 0 {
674            self.cursor.y -= 1
675        } else if self.offset.y > 0 {
676            self.offset.y -= 1;
677        }
678        self.align_cursor();
679        self.print_screen();
680    }
681
682    fn handle_arrow_down(&mut self) {
683        let n = self.lines.len() - 1;
684        let is_eof = n == (self.offset.y + self.cursor.y);
685        let is_bottom = self.cursor.y == rows() - 1;
686        if self.cursor.y < cmp::min(rows(), n) {
687            if is_bottom || is_eof {
688                if !is_eof {
689                    self.offset.y += 1;
690                }
691            } else {
692                self.cursor.y += 1;
693            }
694            self.align_cursor();
695            self.print_screen();
696        }
697    }
698
699    fn cut_line(&mut self) {
700        let i = self.offset.y + self.cursor.y;
701        self.clipboard = Some(self.lines.remove(i));
702        if self.lines.is_empty() {
703            self.lines.push(String::new());
704        }
705        if i == self.lines.len() {
706            self.handle_arrow_up();
707        } else {
708            self.align_cursor();
709            self.print_screen();
710        }
711    }
712
713    fn copy_line(&mut self) {
714        let i = self.offset.y + self.cursor.y;
715        self.clipboard = Some(self.lines[i].clone());
716    }
717
718    fn paste_line(&mut self) {
719        let i = self.offset.y + self.cursor.y;
720        if let Some(line) = self.clipboard.clone() {
721            self.lines.insert(i + 1, line);
722            self.cursor.x = 0;
723            self.offset.x = 0;
724            self.handle_arrow_down(); // Move cursor to pasted line
725        }
726    }
727
728    fn exec(&mut self) -> Option<Cmd> {
729        if let Some(cmd) = prompt(&mut self.command_prompt, ":") {
730            // The cursor is disabled at the beginning of the loop in the `run`
731            // method to avoid seeing it jump around during screen operations.
732            // The `prompt` method above re-enable the cursor so we need to
733            // disable it again until the end of the loop in the `run` method.
734            print!("\x1b[?25l");
735
736            self.exec_command(&cmd)
737        } else {
738            None
739        }
740    }
741
742    fn exec_command(&mut self, cmd: &str) -> Option<Cmd> {
743        let mut res = None;
744        let params: Vec<&str> = match cmd.chars().next() {
745            Some('w') | Some('o') =>  {
746                cmd.split(' ').collect()
747            }
748            _ => {
749                cmd.split('/').collect()
750            }
751        };
752        // TODO: Display line numbers on screen and support command range
753        match params[0] {
754            "d" if params.len() == 1 => { // Delete current line
755                let y = self.offset.y + self.cursor.y;
756                self.lines.remove(y);
757                res = Some(Cmd::Delete);
758            }
759            "%d" if params.len() == 1 => { // Delete all lines
760                self.lines = vec![String::new()];
761                res = Some(Cmd::Delete);
762            }
763            "g" if params.len() == 3 => { // Global command
764                let re = Regex::new(params[1]);
765                if params[2] == "d" { // Delete all matching lines
766                    self.lines.retain(|line| !re.is_match(line));
767                    res = Some(Cmd::Delete);
768                }
769            }
770            "o" | "open" if params.len() == 2 => { // Open
771                self.open_buffer(params[1]);
772                res = Some(Cmd::Open);
773            }
774            "q" | "quit" if params.len() == 1 => { // Quit
775                res = Some(Cmd::Quit);
776            }
777            "s" if params.len() == 4 => { // Substitute current line
778                let re = Regex::new(params[1]);
779                let s = params[2];
780                let y = self.offset.y + self.cursor.y;
781                if params[3] == "g" { // Substitute all occurrences
782                    self.lines[y] = re.replace_all(&self.lines[y], s);
783                } else {
784                    self.lines[y] = re.replace(&self.lines[y], s);
785                }
786                res = Some(Cmd::Replace);
787            }
788            "%s" if params.len() == 4 => { // Substitute all lines
789                let re = Regex::new(params[1]);
790                let s = params[2];
791                let n = self.lines.len();
792                for y in 0..n {
793                    if params[3] == "g" { // Substitute all occurrences
794                        self.lines[y] = re.replace_all(&self.lines[y], s);
795                    } else {
796                        self.lines[y] = re.replace(&self.lines[y], s);
797                    }
798                }
799                res = Some(Cmd::Replace);
800            }
801            "w" | "write" => { // Save file
802                let path = if params.len() == 2 {
803                    params[1]
804                } else {
805                    &self.pathname.clone()
806                };
807                self.save(path).ok();
808                res = Some(Cmd::Save);
809            }
810            _ => {}
811        }
812
813        if res.is_some() {
814            let mut y = self.offset.y + self.cursor.y;
815            let n = self.lines.len() - 1;
816            if y > n {
817                self.cursor.y = n % rows();
818                self.offset.y = n - self.cursor.y;
819                y = n;
820            }
821            let n = self.lines[y].len();
822            if self.offset.x + self.cursor.x > n {
823                self.cursor.x = n % cols();
824                self.offset.x = n - self.cursor.x;
825            }
826
827            self.command_prompt.history.add(cmd);
828            self.command_prompt.history.save(&self.command_history);
829        }
830
831        res
832    }
833
834    pub fn find(&mut self) {
835        if let Some(query) = prompt(&mut self.search_prompt, "Find: ") {
836            if !query.is_empty() {
837                self.search_prompt.history.add(&query);
838                self.search_query = query;
839                self.find_next();
840            }
841        }
842    }
843
844    pub fn find_next(&mut self) {
845        let dx = self.offset.x + self.cursor.x;
846        let dy = self.offset.y + self.cursor.y;
847        for (y, line) in self.lines.iter().enumerate() {
848            let mut o = 0;
849            if y < dy {
850                continue;
851            }
852            if y == dy {
853                o = cmp::min(dx + 1, line.len());
854            }
855            if let Some(i) = line[o..].find(&self.search_query) {
856                let x = o + i;
857                self.cursor.x = x % cols();
858                self.cursor.y = y % rows();
859                self.offset.x = x - self.cursor.x;
860                self.offset.y = y - self.cursor.y;
861                break;
862            }
863        }
864    }
865
866    pub fn open(&mut self) {
867        if let Some(path) = prompt(&mut self.buffer_prompt, "Open: ") {
868            if !path.is_empty() {
869                self.buffer_prompt.history.add(&path);
870                self.open_buffer(&path);
871            }
872        }
873    }
874
875    pub fn open_buffer(&mut self, path: &str) {
876        // Copy current buffer
877        self.buffers[self.buf] = Buffer::from(&*self);
878
879        // Open new buffer
880        let buffer = Buffer::from(path);
881        self.load_buffer(&buffer);
882        self.buf += 1;
883        self.buffers.insert(self.buf, buffer);
884    }
885
886    pub fn next_buffer(&mut self) {
887        self.buffers[self.buf] = Buffer::from(&*self);
888        self.buf = (self.buf + 1) % self.buffers.len();
889        self.load_buffer(&self.buffers[self.buf].clone());
890    }
891
892    pub fn previous_buffer(&mut self) {
893        self.buffers[self.buf] = Buffer::from(&*self);
894        if self.buffers.len() > 1 {
895            if self.buf == 0 {
896                self.buf = self.buffers.len();
897            }
898            self.buf -= 1;
899        }
900        self.load_buffer(&self.buffers[self.buf].clone());
901    }
902
903    pub fn kill_buffer(&mut self) {
904        if self.buffers.len() > 1 {
905            self.previous_buffer();
906            self.buffers.remove((self.buf + 1) % self.buffers.len());
907        }
908    }
909
910    pub fn load_buffer(&mut self, buffer: &Buffer) {
911        self.lines = buffer.lines.clone();
912        self.pathname = buffer.pathname.clone();
913        self.cursor = buffer.cursor.clone();
914        self.offset = buffer.offset.clone();
915        self.highlighted = buffer.highlighted.clone();
916    }
917}
918
919pub fn prompt(prompt: &mut Prompt, label: &str) -> Option<String> {
920    let color = Style::color("black").with_background("silver");
921    let reset = Style::reset();
922
923    // Set up the bottom line for the prompt
924    print!("\x1b[{};1H", rows() + 1);
925    print!("{}{}", color, " ".repeat(cols()));
926    print!("\x1b[{};1H", rows() + 1);
927    print!("\x1b[?25h"); // Enable cursor
928
929    let res = prompt.input(label);
930    print!("{}", reset);
931    res
932}
933
934pub fn rows() -> usize {
935    api::console::rows() - 1 // Leave out one line for status line
936}
937
938pub fn cols() -> usize {
939    api::console::cols()
940}
941
942fn truncated_line_indicator() -> String {
943    let color = Style::color("black").with_background("silver");
944    let reset = Style::reset();
945    format!("{}>{}", color, reset)
946}
947
948fn help() {
949    let csi_option = Style::color("aqua");
950    let csi_title = Style::color("yellow");
951    let csi_reset = Style::reset();
952    println!(
953        "{}Usage:{} edit {}<options> (<path>[:row[:col]])+{1}",
954        csi_title, csi_reset, csi_option
955    );
956    println!();
957    println!("{}Options:{}", csi_title, csi_reset);
958    println!(
959        "  {0}-c{1}, {0}--command <cmd>{1}    Execute command",
960        csi_option, csi_reset
961    );
962}
963
964pub fn main(args: &[&str]) -> Result<(), ExitCode> {
965    let mut paths = Vec::new();
966    let mut cmd = "";
967    let mut i = 1;
968    let n = args.len();
969    while i < n {
970        match args[i] {
971            "-h" | "--help" => {
972                help();
973                return Ok(());
974            }
975            "-c" | "--command" => {
976                if i + 1 < n {
977                    i += 1;
978                    cmd = args[i];
979                } else {
980                    error!("Missing command");
981                    return Err(ExitCode::UsageError);
982                }
983            }
984            _ => {
985                if args[i].starts_with('-') {
986                    error!("Invalid option '{}'", args[i]);
987                    return Err(ExitCode::UsageError);
988                } else {
989                    paths.push(args[i])
990                }
991            }
992        }
993        i += 1;
994    }
995    if paths.is_empty() {
996        help();
997        return Err(ExitCode::UsageError);
998    }
999
1000    let mut editor = Editor::new(paths[0]);
1001    let n = paths.len();
1002    for i in 1..n {
1003        editor.open_buffer(paths[i]);
1004    }
1005
1006    if !cmd.is_empty() {
1007        for _ in 0..n {
1008            editor.next_buffer();
1009            editor.exec_command(cmd);
1010            for line in &editor.lines {
1011                println!("{}", line);
1012            }
1013        }
1014        return Ok(());
1015    }
1016
1017    editor.run()
1018}