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

第三篇

这部分接着处理用户退出命令以及一些其他新功能;

3.1 使用Ctrl+Q退出

rust 复制代码
modifiers: event::KeyModifiers::CONTROL,

使用CONTROL替换之前的NONE值即可;


3.2 重构键盘输入

让我们重构我们的代码,以便我们有一个用于低级按键读取的函数,以及另一个用于将按键映射到编辑器操作的函数。

  • 首先,让我们创建一个 struct 可以读取各种按键的按钮。我们将其命名为:Reader:
rust 复制代码
struct Reader;
  • 然后添加一个方法来读取关键事件:
rust 复制代码
impl Reader {
    fn read_key(&self) -> crossterm::Result<KeyEvent> {
        loop {
            if event::poll(Duration::from_millis(500))? {
                if let Event::Key(event) = event::read()? {
                    return Ok(event);
                }
            }
        }
    }
}
  • 现在让我们创建一个新结构 Editor ,它将是我们项目的主要主脑。
rust 复制代码
struct Editor {
    reader: Reader,
}

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

我们还创建了一个 new 方法来创建 的新 Editor 实例。

  • 现在让我们处理 返回 Reader 的事件并创建一个 run 函数:
rust 复制代码
struct Editor {
    reader: Reader,
}

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

    fn process_keypress(&self) -> crossterm::Result<bool> {
        match self.reader.read_key()? {
            KeyEvent {
                code: KeyCode::Char('q'),
                modifiers: event::KeyModifiers::CONTROL,
            } => return Ok(false),
            _ => {}
        }
        Ok(true)
    }

    fn run(&self) -> crossterm::Result<bool> {
        self.process_keypress()
    }
}

在函数process_keypress中 ,我们返回是否应该继续读取关键事件。如果返回 false,则表示程序应该终止,因为我们不想再次读取关键事件。现在让我们修改一下 main()方法来 改用 Editor.run()

rust 复制代码
fn main() -> crossterm::Result<()> {
    let _clean_up = CleanUp;
    terminal::enable_raw_mode()?;
    /* modify */
    let editor = Editor::new();
    while editor.run()? {}
    /* end */
    Ok(())
}

3.3 屏幕清理

在用户输入之前将屏幕清理干净,这里使用一个Outputstruct来处理输出相关的内容;

rust 复制代码
struct Output;

impl Output {
    fn new() -> Self {
        Self
    }

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

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()
    }
}

clear_screen 函数实际执行的操作是将转义序列写入终端。这些序列修改了终端的行为,并可用于执行其他操作,例如添加颜色等。

  • 修改调用关系:
rust 复制代码
use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, execute, terminal};
use std::io::stdout;
use std::time::Duration; /* add this line */

struct CleanUp;
struct Reader;
struct Editor {
    reader: Reader,
    output:Output,
}
struct Output;

impl Output {
    fn new() -> Self {
        Self
    }

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

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()
    }
}

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

    fn process_keypress(&self) -> crossterm::Result<bool> {
        match self.reader.read_key()? {
            KeyEvent {
                code: KeyCode::Char('q'),
                modifiers: event::KeyModifiers::CONTROL,
                kind: _,
                state: _,
            } => return Ok(false),
            _ => {}
        }
        Ok(true)
    }
    fn run(&self) -> crossterm::Result<bool> {
        self.output.refresh_screen()?;
        self.process_keypress()
    }
}

impl Drop for CleanUp {
    fn drop(&mut self) {
        terminal::disable_raw_mode().expect("Unable to disable raw mode")
    }
}

impl Reader {
    fn read_key(&self) -> crossterm::Result<KeyEvent> {
        loop {
            if event::poll(Duration::from_millis(500))? {
                if let Event::Key(event) = event::read()? {
                    return Ok(event);
                }
            }
        }
    }
}

/// main函数
fn main() -> std::result::Result<(), std::io::Error> {
    let _clean_up = CleanUp;
    terminal::enable_raw_mode()?;
    let editor = Editor::new();
    while editor.run()? {}
    Ok(())
}

3.4 重新定位光标

你可能已经注意到光标未位于屏幕的左上角。这样我们就可以从上到下绘制我们的编辑器。

rust 复制代码
use crossterm::event::*;
use crossterm::terminal::ClearType;
use crossterm::{cursor, event, execute, terminal}; /* add import*/
use std::io::stdout;
use std::time::Duration;

struct CleanUp;

impl Drop for CleanUp {
    fn drop(&mut self) {
        terminal::disable_raw_mode().expect("Unable to disable raw mode")
    }
}

struct Output;

impl Output {
    fn new() -> Self {
        Self
    }

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

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()
    }
}

3.5 退出时清屏

让我们清除屏幕并在程序退出时重新定位光标。

如果在渲染屏幕的过程中发生错误,我们不希望程序的输出留在屏幕上,也不希望将错误打印在光标恰好位于该点的任何位置。

所以当我们的程序成功或失败退出时,我们会将 Cleanup 该函数用于清除屏幕:

Drop中新增: Output::clear_screen().expect("Error");

rust 复制代码
struct CleanUp;

impl Drop for CleanUp {
    fn drop(&mut self) {
        terminal::disable_raw_mode().expect("Unable to disable raw mode");
        Output::clear_screen().expect("Error"); /* add this line*/
    }
}

struct Output;

impl Output {
    fn new() -> Self {
        Self
    }

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

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()
    }
}

3.6 添加波浪号

让我们在屏幕的左侧画一列波浪号 ( ~ ),就像 vim 一样。在我们的文本编辑器中,我们将在正在编辑的文件末尾之后的任何行的开头绘制一个波浪号。

rust 复制代码
struct Output;

impl Output {
    fn new() -> Self {
        Self
    }

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

    /* add this function */
    fn draw_rows(&self) {
        for _ in 0..24 {
            println!("~\r");
        }
    }

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()?;
        /* add the following lines*/
        self.draw_rows();
        execute!(stdout(), cursor::MoveTo(0, 0))
        /* end */
    }
}

draw_rows() 将处理绘制正在编辑的文本缓冲区的每一行。现在,它在每行中绘制一个波浪号,这意味着该行不是文件的一部分,不能包含任何文本。绘制后,我们将光标发送回屏幕的左上角。

  • 现在让我们修改代码以绘制正确数量的波浪号:
rust 复制代码
/* modify */
struct Output {
    win_size: (usize, usize),
}

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

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

    fn draw_rows(&self) {
        let screen_rows = self.win_size.1; /* add this line */
        for _ in 0..screen_rows { /* modify */
            println!("~\r");
        }
    }

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()?;
        self.draw_rows();
        execute!(stdout(), cursor::MoveTo(0, 0))
    }
}

首先,我们修改 Output 以保留窗口大小,因为我们将使用窗口的大小进行多次计算。然后设置创建输出实例时的 win_size 值。 type 中的 win_size 整数是 usize but terminal::size() 返回一个类型 (u16,16) 为 的元组,因此我们必须转换为 u16 usize

也许您注意到屏幕的最后一行似乎没有波浪号。这是因为我们的代码中有一个小错误。当我们打印最终的波浪号时,我们会像在任何其他行上一样打印一个 "\r\n"println!() 添加一个新行),但这会导致终端滚动以便为新的空白行腾出空间。

rust 复制代码
impl Output {
    fn new() -> Self {
        let win_size = terminal::size()
            .map(|(x, y)| (x as usize, y as usize))
            .unwrap();
        Self { win_size }
    }

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

    fn draw_rows(&self) {
        let screen_rows = self.win_size.1;
        /* modify */
        for i in 0..screen_rows {
            print!("~");
            if i < screen_rows - 1 {
                println!("\r")
            }
            stdout().flush();
        }
        /* end */
    }

    fn refresh_screen(&self) -> crossterm::Result<()> {
        Self::clear_screen()?;
        self.draw_rows();
        execute!(stdout(), cursor::MoveTo(0, 0))
    }
}

3.7 追加缓冲区

由于在屏幕每次刷新时都会进行绘制,导致有闪频的问题。

rust 复制代码
struct EditorContents {
    content: String,
}

impl EditorContents {
    
    fn new() -> Self {
        Self {
            content: String::new(),
        }
    }
    
    fn push(&mut self, ch: char) {
        self.content.push(ch)
    }

    fn push_str(&mut self, string: &str) {
        self.content.push_str(string)
    }
}
rust 复制代码
impl io::Write for EditorContents {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match std::str::from_utf8(buf) {
            Ok(s) => {
                self.content.push_str(s);
                Ok(s.len())
            }
            Err(_) => Err(io::ErrorKind::WriteZero.into()),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        let out = write!(stdout(), "{}", self.content);
        stdout().flush()?;
        self.content.clear();
        out
    }
}
  • 首先,我们将传递到 write 函数的字节转换为 str,以便我们可以将其添加到 content
  • 如果字节可以转换为字符串,则返回字符串的长度,否则返回错误。当我们在 EditorContents 上调用 flush() 时,我们希望它写入 stdout,因此我们使用 write!() 宏,然后调用 stdout.flush()
  • 我们还必须清除 content,以便我们可以在下一次屏幕刷新时使用。
  • 使用EditorContents:
rust 复制代码
use crossterm::{cursor, event, execute, queue, terminal}; /* modify */

struct Output {
    win_size: (usize, usize),
    editor_contents: EditorContents, /* add this line */
}

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) { /* modify */
        let screen_rows = self.win_size.1;
        for i in 0..screen_rows {
            self.editor_contents.push('~'); /* modify */
            if i < screen_rows - 1 {
                self.editor_contents.push_str("\r\n"); /* modify */
            }
        }
    }

    fn refresh_screen(&mut self) -> crossterm::Result<()> { /* modify */
        queue!(self.editor_contents, terminal::Clear(ClearType::All), cursor::MoveTo(0, 0))?; /* add this line*/
        self.draw_rows();
        queue!(self.editor_contents, cursor::MoveTo(0, 0))?; /* modify */
        self.editor_contents.flush() /* add this line*/
    }
}

注意,我们已更改 draw_rows 为使用 &mut self ,因此我们需要对之前的部分代码做一下调整:

rust 复制代码
fn run(&mut self) -> crossterm::Result<bool> { /* modify */
    self.output.refresh_screen()?;
    self.process_keypress()
}
rust 复制代码
fn main() -> crossterm::Result<()> {
    let _clean_up = CleanUp;
    terminal::enable_raw_mode()?;
    let mut editor = Editor::new(); /* modify */
    while editor.run()? {}
    Ok(())
}

烦人的闪烁效果还有另一个可能的来源。当终端绘制到屏幕时,光标可能会在屏幕中间的某个地方显示一瞬间。

为确保不会发生这种情况,让我们在刷新屏幕之前隐藏光标,并在刷新完成后立即再次显示光标。

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('~');
            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, //add this
            terminal::Clear(ClearType::All),
            cursor::MoveTo(0, 0)
        )?;
        self.draw_rows();
        queue!(
            self.editor_contents,
            cursor::MoveTo(0, 0),
            /* add this */ cursor::Show
        )?;
        self.editor_contents.flush()
    }
}

本期完,下期内容抢先知:

  • 逐行清除
  • 添加欢迎和版本信息
  • 按键移动光标
  • 方向键移动光标
  • 光标移动溢出问题
  • 分页和首尾页
相关推荐
hikktn2 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程2 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
幺零九零零3 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
23zhgjx-NanKon4 小时前
华为eNSP:QinQ
网络·安全·华为
23zhgjx-NanKon4 小时前
华为eNSP:mux-vlan
网络·安全·华为
点点滴滴的记录4 小时前
RPC核心实现原理
网络·网络协议·rpc
Lionhacker5 小时前
网络工程师这个行业可以一直干到退休吗?
网络·数据库·网络安全·黑客·黑客技术
程思扬5 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
ZachOn1y5 小时前
计算机网络:运输层 —— 运输层概述
网络·tcp/ip·计算机网络·运输层