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

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

4.1 逐行清除

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

rust 复制代码
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 添加版本信息

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

rust 复制代码
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 消息。然后检查长度是否大于屏幕一次可以显示的长度。如果大于,则将其截断。

  • 现在处理下居中
rust 复制代码
 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结构体来存储光标信息
rust 复制代码
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
rust 复制代码
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 方法来控制各种按键的移动逻辑:
rust 复制代码
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于所有的输出都有交互。

rust 复制代码
impl Output {
    
    ...
    
    
    fn move_cursor(&mut self,direction:char) {
        self.cursor_controller.move_cursor(direction);
    }
}
  • 修改process_keyprocess(),将按下的按键信息传递给move_cursor();
rust 复制代码
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变量,然后检查该变量的取值是否满足给定的四个方向键的字符;

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

    rust 复制代码
     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待修复),接下来就是实现方向键的移动控制功能。

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

rust 复制代码
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)
}
rust 复制代码
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!(),
        }
    }
}
rust 复制代码
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扼杀在摇篮之中。

rust 复制代码
struct CurSorController {
    cursor_x: usize,
    cursor_y: usize,
    screen_columns:usize,
    screen_rows:usize,
}
rust 复制代码
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
rust 复制代码
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 翻页和结束

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

rust 复制代码
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的实现也很简单:

rust 复制代码
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 键。

相关推荐
时之彼岸Φ28 分钟前
Web:HTTP包的相关操作
网络·网络协议·http
W215529 分钟前
LINUX网络编程:http
网络·网络协议·http
VinciYan1 小时前
Rust使用Actix-web和SeaORM库开发WebAPI通过Swagger UI查看接口文档
rust·api·web·orm
Mogu_cloud1 小时前
pcdn盒子连接方式
网络·智能路由器
Hqst_Kevin1 小时前
Hqst 品牌 H81801D 千兆 DIP 网络变压器在光猫收发器机顶盒中的应用
运维·服务器·网络·5g·网络安全·信息与通信·信号处理
Hqst 网络变压器 Andy1 小时前
交换机最常用的网络变压器分为DIP和SM
网络·依赖倒置原则
网安康sir2 小时前
2024年三个月自学手册 网络安全(黑客技术)
网络·安全·web安全
Nigoridl2 小时前
MSF的使用学习
网络·web安全
计算机学姐2 小时前
基于PHP的电脑线上销售系统
开发语言·vscode·后端·mysql·编辑器·php·phpstorm
JANGHIGH3 小时前
VSCode值得推荐的插件(持续更新中)
ide·vscode·编辑器