Xed编辑器开发第三期:使用Rust从0到1写一个文本编辑器

继续Xed编辑器开发第二期:使用Rust从0到1写一个文本编辑器的开发进度,这是第三期的内容:

4.1 逐行清除

在每次刷新之前清除整个屏幕似乎不太理想,最好在重新绘制每行时清除每行。让我们删除 Clear(ClearType::All),而是在我们绘制的每行的末尾使用Clear(ClearType::UntilNewLine)

impl Output {
    fn new() -> Self {
        let win_size = terminal::size()
            .map(|(x, y)| (x as usize, y as usize))
            .unwrap();
        Self {
            win_size,
            editor_contents: EditorContents::new(),
        }
    }

    fn clear_screen() -> crossterm::Result<()> {
        execute!(stdout(), terminal::Clear(ClearType::All))?;
        execute!(stdout(), cursor::MoveTo(0, 0))
    }

    fn draw_rows(&mut self) {
        let screen_rows = self.win_size.1;
        for i in 0..screen_rows {
            self.editor_contents.push('~');
            //add the following
            queue!(
                self.editor_contents,
                terminal::Clear(ClearType::UntilNewLine)
            )
            .unwrap();
            //end
            if i < screen_rows - 1 {
                self.editor_contents.push_str("\r\n");
            }
        }
    }

    fn refresh_screen(&mut self) -> crossterm::Result<()> {
        //modify
        queue!(self.editor_contents, cursor::Hide, cursor::MoveTo(0, 0))?;
        self.draw_rows();
        queue!(self.editor_contents, cursor::MoveTo(0, 0), cursor::Show)?;
        self.editor_contents.flush()
    }
}

4.2 添加版本信息

是时候了,让我们简单地在屏幕下方的三分之一处显示编辑器的名称和版本。

const VERSION: &str = "0.0.1";

impl Output{
    ...
    
    fn draw_rows(&mut self) {
        let screen_rows = self.win_size.1;
        let screen_columns = self.win_size.0; // add this line
        for i in 0..screen_rows {
            // add the following
            if i == screen_rows / 3 {
                let mut welcome = format!("X Editor --- Version {}", VERSION);
                if welcome.len() > screen_columns {
                    welcome.truncate(screen_columns)
                }
                self.editor_contents.push_str(&welcome);
            } else {
                self.editor_contents.push('~');
            }
            /* end */
            queue!(
                self.editor_contents,
                terminal::Clear(ClearType::UntilNewLine)
            )
            .unwrap();
            if i < screen_rows - 1 {
                self.editor_contents.push_str("\r\n");
            }
        }
    }
}

我们使用 format!() 宏来加入 VERSION 消息。然后检查长度是否大于屏幕一次可以显示的长度。如果大于,则将其截断。

  • 现在处理下居中
 impl Output {
    
     ...
     
    fn draw_rows(&mut self) {
        let screen_rows = self.win_size.1;
        let screen_columns = self.win_size.0;
        for i in 0..screen_rows {
            if i == screen_rows / 3 {
                let mut welcome = format!("Pound Editor --- Version {}", VERSION);
                if welcome.len() > screen_columns {
                    welcome.truncate(screen_columns)
                }
                /* add the following*/
                let mut padding = (screen_columns - welcome.len()) / 2;
                if padding != 0 {
                    self.editor_contents.push('~');
                    padding -= 1
                }
                (0..padding).for_each(|_| self.editor_contents.push(' '));
                self.editor_contents.push_str(&welcome);
                /* end */
            } else {
                self.editor_contents.push('~');
            }
            queue!(
                self.editor_contents,
                terminal::Clear(ClearType::UntilNewLine)
            )
            .unwrap();
            if i < screen_rows - 1 {
                self.editor_contents.push_str("\r\n");
            }
        }
    }
}

使字符串居中,可以将屏幕宽度除以 2,然后从中减去字符串长度的一半。换言之: screen_columns/2 - welcome.len()/2 ,简化为 (screen_columns - welcome.len()) / 2 。这告诉你应该从屏幕左边缘开始打印字符串的距离。因此,我们用空格字符填充该空间,除了第一个字符,它应该是波浪号


4.3 移动光标

现在让我们转到光标的控制上。目前,箭头键和其他任何键都不能移动游标。让我们从使用 wasd 键移动游标开始。

  • 新建一个CursorController结构体来存储光标信息
struct CursorController {
    cursor_x: usize,
    cursor_y: usize,
}

impl CursorController {
    fn new() -> CursorController {
        Self {
            cursor_x: 0,
            cursor_y: 0,
        }
    }
}

cursor_x 是光标的水平坐标(列), cursor_y 是垂直坐标(行)。我们将它们初始化为 0 ,因为我们希望光标从屏幕的左上角开始。

  • 现在让我们向 Output struct and update refresh_screen() 添加一个 cursor_controller 字段以使用 cursor_xcursor_y
struct Output {
    win_size: (usize, usize),
    editor_contents: EditorContents,
    cursor_controller: CursorController, // add this field
}

impl Output {
    
    fn new() -> Self {
        let win_size = terminal::size()
            .map(|(x, y)| (x as usize, y as usize))
            .unwrap();
        Self {
            win_size,
            editor_contents: EditorContents::new(),
            cursor_controller: CursorController::new(), /* add initializer*/
        }
    }

    fn clear_screen() -> crossterm::Result<()> {
        execute!(stdout(), terminal::Clear(ClearType::All))?;
        execute!(stdout(), cursor::MoveTo(0, 0))
    }

    fn draw_rows(&mut self) {
        let screen_rows = self.win_size.1;
        let screen_columns = self.win_size.0;
        for i in 0..screen_rows {
            if i == screen_rows / 3 {
                let mut welcome = format!("Xed Editor --- Version {}", VERSION);
                if welcome.len() > screen_columns {
                    welcome.truncate(screen_columns)
                }
                let mut padding = (screen_columns - welcome.len()) / 2;
                if padding != 0 {
                    self.editor_contents.push('~');
                    padding -= 1
                }
                (0..padding).for_each(|_| self.editor_contents.push(' '));
                self.editor_contents.push_str(&welcome);
            } else {
                self.editor_contents.push('~');
            }
            queue!(
                self.editor_contents,
                terminal::Clear(ClearType::UntilNewLine)
            )
            .unwrap();
            if i < screen_rows - 1 {
                self.editor_contents.push_str("\r\n");
            }
        }
    }

    fn refresh_screen(&mut self) -> crossterm::Result<()> {
        queue!(self.editor_contents, cursor::Hide, cursor::MoveTo(0, 0))?;
        self.draw_rows();
        /* modify */
        let cursor_x = self.cursor_controller.cursor_x;
        let cursor_y = self.cursor_controller.cursor_y;
        queue!(
            self.editor_contents,
            cursor::MoveTo(cursor_x as u16, cursor_y as u16),
            cursor::Show
        )?;
        /* end */
        self.editor_contents.flush()
    }
}
  • 现在我们添加一个 CursorController 方法来控制各种按键的移动逻辑:
impl CursorController {
    fn new() -> CursorController {
        Self {
            cursor_x: 0,
            cursor_y: 0,
        }
    }

    /* add this function */
    fn move_cursor(&mut self, direction: char) {
        match direction {
            'w' => {
                self.cursor_y -= 1;
            }
            'a' => {
                self.cursor_x -= 1;
            }
            's' => {
                self.cursor_y += 1;
            }
            'd' => {
                self.cursor_x += 1;
            }
            _ => unimplemented!(),
        }
    }
}

这段逻辑很简单,就不过多解释了。

接下来是修改在Output中使用该方法,因为我们希望通过这个struct于所有的输出都有交互。

impl Output {
    
    ...
    
    
    fn move_cursor(&mut self,direction:char) {
        self.cursor_controller.move_cursor(direction);
    }
}
  • 修改process_keyprocess(),将按下的按键信息传递给move_cursor();
impl Editor {
    fn new() -> Self {
        Self {
            reader: Reader,
            output: Output::new(),
        }
    }

    fn process_keypress(&mut self) -> crossterm::Result<bool> { /* modify*/
        match self.reader.read_key()? {
            KeyEvent {
                code: KeyCode::Char('q'),
                modifiers: KeyModifiers::CONTROL,
            } => return Ok(false),
            /* add the following*/
            KeyEvent {
                code: KeyCode::Char(val @ ('w' | 'a' | 's' | 'd')),
                modifiers: KeyModifiers::NONE,
            } => self.output.move_cursor(val),
            // end
            _ => {}
        }
        Ok(true)
    }

    fn run(&mut self) -> crossterm::Result<bool> {
        self.output.refresh_screen()?;
        self.process_keypress()
    }
}
  • 这里使用了@运算符。它的基本作用是创建一个变量并检查该变量是否提供了对于的匹配条件;

  • 因此在这种情况下它创建了val变量,然后检查该变量的取值是否满足给定的四个方向键的字符;

  • 所以,这段逻辑也类似于下面的写法:

     fn process_keypress(&mut self) -> crossterm::Result<bool> {
            match self.reader.read_key()? {
                KeyEvent {
                    code: KeyCode::Char('q'),
                    modifiers: KeyModifiers::CONTROL,
                } => return Ok(false),
                /* note the following*/
                KeyEvent {
                    code: KeyCode::Char(val),
                    modifiers: KeyModifiers::NONE,
                }  => {
                    match val {
                        'w'| 'a'|'s'|'d' => self.output.move_cursor(val),
                        _=> {/*do nothing*/}
                    }
    
                },
                // end
                _ => {}
            }
            Ok(true)
        }
    

现在如果你运行程序并移动光标可能会出现异常终止程序,这是由于溢出导致的OutOfBounds错误,后面会解决。


4.4 使用箭头移动光标

到这里为止,我们已经实现了指定字符按键的移动操作(尽管还有些BUG待修复),接下来就是实现方向键的移动控制功能。

实现上和上面的功能很类似,只需要对原代码进行简单的修改调整即可。

fn process_keypress(&mut self) -> crossterm::Result<bool> {
    /* modify*/
    match self.reader.read_key()? {
        KeyEvent {
            code: KeyCode::Char('q'),
            modifiers: KeyModifiers::CONTROL,
        } => return Ok(false),
        /* modify the following*/
        KeyEvent {
            code: direction @ (KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right),
            modifiers: KeyModifiers::NONE,
        } => self.output.move_cursor(direction),
        // end
        _ => {}
    }
    Ok(true)
}
impl CursorController {
    fn new() -> CursorController {
        Self {
            cursor_x: 0,
            cursor_y: 0,
        }
    }

    /* modify the function*/
    fn move_cursor(&mut self, direction: KeyCode) {
        match direction {
            KeyCode::Up => {
                self.cursor_y -= 1;
            }
            KeyCode::Left => {
                self.cursor_x -= 1;
            }
            KeyCode::Down => {
                self.cursor_y += 1;
            }
            KeyCode::Right => {
                self.cursor_x += 1;
            }
            _ => unimplemented!(),
        }
    }
}
impl Output {
    
    ...
    
    fn move_cursor(&mut self, direction: KeyCode) { //modify
        self.cursor_controller.move_cursor(direction);
    }
    
    ...
}

4.5 修复光标移动时的越界问题

应该你还记得前面留下的一个BUG,如果记不得了就再去复习一遍,因为即使改用了方向键来移动光标,这个BUG依旧是存在的。

所以这小节主要就是解决这个问题来的。

会出现越界的异常,是因为我们定义的光标坐标的变量cursor_xcursor_y类型是usize,不能为负数。但这一点在我们移动时并不会得到保障,一旦移动导致负数的出现,那么程序就会panic

因为,解决这个问题的手段就是做一下边界判断,将BUG扼杀在摇篮之中。

struct CurSorController {
    cursor_x: usize,
    cursor_y: usize,
    screen_columns:usize,
    screen_rows:usize,
}
impl CursorController {
    /* modify */
    fn new(win_size: (usize, usize)) -> CursorController {
        Self {
            cursor_x: 0,
            cursor_y: 0,
            screen_columns: win_size.0,
            screen_rows: win_size.1,
        }
    }

    /* modify the function*/
    fn move_cursor(&mut self, direction: KeyCode) {
        match direction {
            KeyCode::Up => {
                self.cursor_y = self.cursor_y.saturating_sub(1);
            }
            KeyCode::Left => {
                if self.cursor_x != 0 {
                    self.cursor_x -= 1;
                }
            }
            KeyCode::Down => {
                if self.cursor_y != self.screen_rows - 1 {
                    self.cursor_y += 1;
                }
            }
            KeyCode::Right => {
                if self.cursor_x != self.screen_columns - 1 {
                    self.cursor_x += 1;
                }
            }
            _ => unimplemented!(),
        }
    }
}
  1. 向上移动(Up)
    • 使用 saturating_sub 方法来确保不会出现溢出,即当 self.cursor_y 为 0 时,减去 1 后不会变为负数,而是保持为 0。
  2. 向左移动(Left)
    • 如果 self.cursor_x 不等于 0,则将 self.cursor_x 减去 1。
  3. 向下移动(Down)
    • 如果 self.cursor_y 不等于 self.screen_rows - 1,则将 self.cursor_y 加上 1,确保不会超出屏幕的底部。
  4. 向右移动(Right)
    • 如果 self.cursor_x 不等于 self.screen_columns - 1,则将 self.cursor_x 加上 1,确保不会超出屏幕的右侧。
  • 修改 Output struct
impl Output {
    
    fn new() -> Self {
        let win_size = terminal::size()
            .map(|(x, y)| (x as usize, y as usize))
            .unwrap();
        Self {
            win_size,
            editor_contents: EditorContents::new(),
            cursor_controller: CursorController::new(win_size), /* modify initializer*/
        }
    }
    
    ...

}

4.6 翻页和结束

本小节主要是实现上下翻页(快速跳页)以及首页末页的实现;

impl Editor {
    fn new() -> Self {
        Self {
            reader: Reader,
            output: Output::new(),
        }
    }

    fn process_keypress(&mut self) -> crossterm::Result<bool> {
        match self.reader.read_key()? {
            KeyEvent {
                code: KeyCode::Char('q'),
                modifiers: KeyModifiers::CONTROL,
            } => return Ok(false),
            KeyEvent {
                code:
                    direction
                    @
                    (KeyCode::Up
                    | KeyCode::Down
                    | KeyCode::Left
                    | KeyCode::Right
                    | KeyCode::Home
                    | KeyCode::End),
                modifiers: KeyModifiers::NONE,
            } => self.output.move_cursor(direction),
            KeyEvent {
                code: val @ (KeyCode::PageUp | KeyCode::PageDown),
                modifiers: KeyModifiers::NONE,
            } =>/*add this */  (0..self.output.win_size.1).for_each(|_| {
                self.output.move_cursor(if matches!(val, KeyCode::PageUp) {
                    KeyCode::Up
                } else {
                    KeyCode::Down
                });
            }),
            _ => {}
        }
        Ok(true)
    }

    fn run(&mut self) -> crossterm::Result<bool> {
        self.output.refresh_screen()?;
        self.process_keypress()
    }
}

如果您使用的是带有 Fn 按键的笔记本电脑,则可以按 Fn+↑ 下并 Fn+↓ 模拟按下 Page UpPage Down 键。

对于HomeEnd的实现也很简单:

fn move_cursor(&mut self, direction: KeyCode) {
    match direction {
        KeyCode::Up => {
            self.cursor_y = self.cursor_y.saturating_sub(1);
        }
        KeyCode::Left => {
            if self.cursor_x != 0 {
                self.cursor_x -= 1;
            }
        }
        KeyCode::Down => {
            if self.cursor_y != self.screen_rows - 1 {
                self.cursor_y += 1;
            }
        }
        KeyCode::Right => {
            if self.cursor_x != self.screen_columns - 1 {
                self.cursor_x += 1;
            }
        }
        /* add the following*/
        KeyCode::End => self.cursor_x = self.screen_columns - 1,
        KeyCode::Home => self.cursor_x = 0,
        _ => unimplemented!(),
    }
}

如果您使用的是带有 Fn 键的笔记本电脑,则可以按 Fn + ←HomeFn + → 模拟按下 和 End 键。

相关推荐

  1. React富文本编辑器开发(十一)命令与编辑器

    2024-05-25 22:10:33       18 阅读
  2. rust0开始项目-04-多样化错误处理

    2024-05-25 22:10:33       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-05-25 22:10:33       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-05-25 22:10:33       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-05-25 22:10:33       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-05-25 22:10:33       20 阅读

热门阅读

  1. 【数据结构与算法 | 基础篇】环形数组模拟队列

    2024-05-25 22:10:33       16 阅读
  2. 如何抓到微软模拟飞行.dmp文件

    2024-05-25 22:10:33       8 阅读
  3. 在Ubuntu20.04.6上编译Qt5.15.2源码并安装

    2024-05-25 22:10:33       12 阅读
  4. ROS+UBUNTU开发常用指令

    2024-05-25 22:10:33       11 阅读
  5. Ubuntu 安装libpng12的方法

    2024-05-25 22:10:33       10 阅读
  6. 前端中css穿透样式:deep的用法

    2024-05-25 22:10:33       11 阅读
  7. 面向对象的三大特征和五大基本原则

    2024-05-25 22:10:33       10 阅读
  8. android 水平居中对齐并举例

    2024-05-25 22:10:33       12 阅读
  9. 过滤器 -- Filter

    2024-05-25 22:10:33       8 阅读
  10. 【sass数字运算简介以及使用方法】

    2024-05-25 22:10:33       12 阅读