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

  • 这是一个使用Rust实现的轻量化文本编辑器。
  • 学过Rust的都知道,Rust 从入门到实践中间还隔着好几个Go语言的难度,因此,如果你也正在学习Rust,那么恭喜你,这个项目被你捡到了。
  • 本项目内容较多,大概会分三期左右陆续发布,欢迎关注!

1. 第一篇

本系列教程默认你已经配置了Rust开发环境并具有一定的rust基础。所以直接从项目创建开始讲解;

使用下面的命令创建项目

  • 项目创建
cargo new xed
  • 运行程序
cargo run

如果成功输出Hello World表示项目基本功能正常,本章节完!


2. 第二篇

2.1 读取用户输入

现在修改main.rs,尝试读取用户的输入,你可以随时按下Ctrl + c终止程序;

use std::io;
use std::io::Read;
fn main() {let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 {}
}
  • 这里的内容不多,主要涉及到io的基本操作,所以导包是必要的;
  • 第4行创建了一个可变的buf数组,长度为1,初始值为0;
  • io::stdin().read(&mut buf) 尝试从标准输入流中读取数据,并将其存储在 buf 中。read 方法返回一个 Result 类型,其中包含读取的字节数或一个错误。
  • 所以expect("Failed to read line") 用于处理可能出现的错误情况。如果读取失败,程序将打印出 “Failed to read line” 作为错误信息并终止程序。
  • 最后的==1检查读取的字节数是否为1,否则结束循环;

2.2 实现q命令

本小节实现基本功能:用户输入q按下回车执行退出程序的操作。

use std::io;
use std::io::Read;
fn main() {let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf !=[b'q'] {}
}
  • 程序会检查buf中输入的每一个字符,如果与q相同,就会结束程序;

在 Rust 中,[b'q'] 是一个字节字符串字面量,表示一个包含单个字节 q 的字节数组。

  1. [b'q']

    • b'q' 是 Rust 中的字节字面量,表示一个字节,即 ASCII 字符 'q' 对应的字节值。
    • 在 Rust 中,使用 b 前缀可以将字符转换为对应的字节值。这种表示方式常用于处理字节数据。
  2. 字节值和字符映射:

    • 在 ASCII 编码中,每个字符都有一个对应的字节值。在 ASCII 编码中,字符 'q' 对应的字节值是 113
    • 使用 b'q' 可以直接表示这个字节值,而 [b'q'] 则将这个字节值包装在一个长度为 1 的字节数组中。

因此,[b'q'] 表示一个包含单个字节值为 113(即 ASCII 字符 'q' 对应的字节值)的字节数组。在上下文中,buf != [b'q'] 的条件判断将检查 buf 中存储的字节是否不等于 'q' 对应的字节值,即检查输入的数据是否不是 'q'

  • 等价写法:buf[0] != b'q'

2.3 常规模式与原始模式

上面的情况就是常规模式,也就是程序启动后终端可以正常监听并回显你输入的内容;

而这里说的原始模式的作用和常规模式相反,我们这里可以直接使用crossterm库来实现,添加依赖:

cargo add crossterm
use std::io;
use std::io::Read;
use crossterm::terminal; // 添加依赖
fn main() {terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}
}

现在如果你运行程序,你的输入在终端并没有任何回显,并且当你输入q的时候也是直接无提示的退出程序,这就是crossterm帮我们实现的原始模式的基本功能;

如果要禁用原始模式,考虑下面的代码,最后一行就是禁用这个模式的逻辑;

use crossterm::terminal; /* add this line */
use std::io;
use std::io::Read;
fn main() {terminal::enable_raw_mode().expect("Could not turn on Raw mode");let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}terminal::disable_raw_mode().expect("Could not turn off raw mode"); /* add this line */
}

但是这样运行后会出现一个错误:

当在 terminal::enable_raw_mode() 之后的函数中发生错误并导致 panic 时,disable_raw_mode() 将不会被调用,导致终端保持在原始模式。这种情况可能会导致程序结束时终端状态不正确,用户体验受到影响。

所以为了解决这个问题,让我们创建 一个 名为 CleanUpstruct;

struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}

然后修改原来的代码:

use crossterm::terminal; // 添加依赖
use std::io;
use std::io::Read;
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp; // 看这里terminal::enable_raw_modde().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}// terminal::disable_raw_mode().expect("Could not turn off raw mode"); /* add this line */panic!(""); // 看这里
}
  • 现在我们新增了一个struct并实现了Drop这个trait;此时drop()函数会在我们的struct实例,也就是_clean_up超出作用域或者该实例出现panic时候执行;

  • 一旦上面的情况发生,drop()被执行,那么将成功禁用原始模式;

但是现在还有问题,此时使用Ctrl +c 无法退出程序;不妨看看当我们按下这些按键的时候输出了什么东西;

fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {let character = buf[0] as char;if character.is_control() {println!("{}\r", character as u8)} else {println!("{}\r", character)}}
}
  • is_control()判断按下的是否为控制键位,在正常情况下,控制键位输入的字符我们并不需要;
  • ASCII的0-31都是控制字符,127也是;
  • 所以32-126就是可打印的字符,也是我们在编辑文本时需要进行输入回显的;
  • 另外,请注意我们在打印信息的时候使用的是\r而不是\n;此时我们在终端输入数据之后,光标会自动调整到屏幕的左侧。

现在请运行程序并尝试按下控制键位,例如方向键、 或 Escape 、 或 Page Up Page DownHome End Backspace DeleteEnter 或 。尝试使用 Ctrl 组合键,如 Ctrl-A、Ctrl-B 等。你会发现:

  • 方向键:Page Up、Page Down、Home 和 End 都向终端输入 3 或 4 个字节: 27 、、 '[' ,然后是一两个其他字符。这称为转义序列。所有转义序列都以 27 字节开头。按 Escape 键发送单个 27 字节作为输入。

  • Backspace 是字节 127 。Delete 是一个 4 字节的转义序列。

  • Enter 是 byte 10 ,这是一个换行符,也称为 '\n' 或 byte 13 ,这是回车符,也称为 \r

  • 另外:Ctrl-A 1 Ctrl-B2 Ctrl-C3…这确实有效的 将Ctrl 组合键将字母 A-Z 映射到代码 1-26

通过上面的步骤,我们基本了解了按键是如何转为字节的。


2.4 crossterm提供的事件抽象

crossterm 还提供了对各种关键事件的抽象,因此我们不必记住上面那一堆映射关系;而是使用这个crate带来的实现方法;

下面是使用这些抽象重构之火的main.rs:

use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal}; // 添加依赖
use std::io;
use std::io::Read;
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];// 从这里开始重构loop {if let Event::Key(event) = event::read().expect("Failed to read line") {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: event::KeyEventKind::Press,state: event::KeyEventState::NONE,} => break,_ => {// todo}}println!("{:?}\r", event);};}
}
  • Event 是一个 enum 。由于我们目前只对按键感兴趣,因此我们检查返回的 Event 键是否为 Key .然后,我们检查按下的键是否为 q 。如果用户按下 q ,我们就会中断 loop ,程序将终止。
  • 当然,枚举中其他几个字段也是必须的,参考下文档中枚举的定义如下:
pub struct KeyEvent {pub code: KeyCode,pub modifiers: KeyModifiers,pub kind: KeyEventKind,pub state: KeyEventState,
}

其中的kind也是枚举:

pub enum KeyEventKind {Press,Repeat,Release,
}

sate的定义:

    pub struct KeyEventState: u8 {/// The key event origins from the keypad.const KEYPAD = 0b0000_0001;/// Caps Lock was enabled for this key event.////// **Note:** this is set for the initial press of Caps Lock itself.const CAPS_LOCK = 0b0000_1000;/// Num Lock was enabled for this key event.////// **Note:** this is set for the initial press of Num Lock itself.const NUM_LOCK = 0b0000_1000;const NONE = 0b0000_0000;}

看着有点怕但是不要怕,当下只需要理解代码中按下q执行程序退出的逻辑就可以。

下面是一个示例输出,它会在你按下按键的时候记录并打印相关的事件信息。你可以测试一下按下q是否正常退出程序。

image-20240514221935305


2.4 超时处理

现在的情况是,read()会无限期的在等待我们的键盘输入后返回。如果我们一直没有输入,那它就已知等待,这是个问题。因此我们需要有一个超时处理的逻辑,比如超过一定时间没用户没有任何操作就执行超时对应的处理逻辑。

use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal}; // 添加依赖
use std::io;
use std::io::Read;
use std::time::Duration; // 新增依赖
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];// 从这里开始重构loop {if event::poll(Duration::from_millis(500)).expect("Program timed out") { // 超时处理if let Event::Key(event) = event::read().expect("Failed to read line") {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: event::KeyEventKind::Press,state: event::KeyEventState::NONE,} => break,_ => {// todo}}println!("{:?}\r", event);};}}
}

上面的代码中新增的超时处理中用到了crossterm::event::poll这个方法,如果在给定时间内没有 Event 可用, poll 则返回 false ,具体的函数定义信息如下:

image-20240515084930391


2.5 错误处理

一路走来,我们对程序的错误处理都是使用expect()进行简单的捕获,这显然并不是一个很好的选择和习惯,下面通过使用Result来对错误进行进一步的处理,修改main.rs:

use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal};
use std::time::Duration; /* add this line */struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Unable to disable raw mode")}
}fn main() -> std::result::Result<(), std::io::Error> {let _clean_up = CleanUp;terminal::enable_raw_mode()?;loop {if event::poll(Duration::from_millis(500))? {if let Event::Key(event) = event::read()? {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: _,state: _,} => break,_ => {//todo}}println!("{:?}\r", event);};} else {println!("No input yet\r");}}Ok(())
}

修改部分如下,注意,对于main方法本身也是指定了返回值类型,这在下面的贴图中没有展现。

image-20240515090210813

  • ? 算符只能用于返回 Result 的方法中,因此 Option 我们必须修改 our main() 以返回 Result .可以 crossterm::Result<T> 扩展为 std::result::Result<T, std::io::Error>

  • 因此,对于我们的 main() 函数,返回类型可以转换为 std::result::Result<(), std::io::Error>

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

  • Ctrl+Q退出
  • 键盘输入重构
  • 屏幕清理
  • 光标定位
  • 退出清屏
  • 波浪号占位符(类似于vim)
  • 追加缓冲区

写在最后:

如果这篇内容跟下来,你还是觉得比较难,那么我推荐你暂时放一下,这里推荐一个我之前写的开源项目untools,这也是一个使用Rust编写的工具库,可以拿来练手,顺手点个star的同时也欢迎有想法有能力的同学PR;
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/703996.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年第十届中西部外语翻译大赛(1)

2024年第十届中西部外语翻译大赛 竞赛信息 “由中西部翻译协会共同体指导发起&#xff0c;各省市译协共建学术指导委员会&#xff0c;2024年第十届中西部外语翻译大赛由中西部翻译协会共同体秘书处&#xff08;武汉公仪网络科技有限公司&#xff09;承办。” - 获奖证书样图 -…

大模型来了,创业者怎么做出好产品?

大模型的问世惊艳了人们的目光&#xff0c;打开了对AI想象力——生成未来&#xff0c;是谁的未来&#xff1f; “电的发明并不是只能让爱迪生的公司成为全球最大公司&#xff0c;而是为众多电器制造商也提供了巨大的商机。从人类科技史的角度来看&#xff0c;应用层面的价值往…

神经网络复习--循环神经网络

文章目录 RNNLSTM神经网络GAN神经网络 RNN 有些任务人工神经网络&#xff0c;CNN解决不了&#xff0c;自然语言处理中&#xff0c;输入和输出之间不独立&#xff0c;而传统神经网络中&#xff0c;输入和输出都是相互独立的&#xff0c;因此需要一种神经网络让输出和之前的输入…

Java面试题:Spring框架除了IOC和AOP,还有哪些好玩的设计模式?

Spring是一个基于Java的企业级应用程序开发框架&#xff0c;它使用了多种设计模式来实现其各种特性和功能。本文将介绍一些在Spring中使用的常见设计模式以及相应的代码示例和说明。 单例模式 单例模式是Spring中最常用的设计模式之一。在ApplicationContext中&#xff0c;Bean…

GPT-4o、GPT-4国内可用!新UI界面率先体验方法!

测试情况&#xff1a; 现根据测试结果&#xff0c;先对比一下普号4o和付费的区别&#xff1a; 注&#xff1a; plus限制情况&#xff1a;4的次数用完后可以用4o&#xff0c;但4o的80条用完后不能用4&#xff1b; team账户限制是100条/3h&#xff0c;4o和4共享额度 目前发现的…

SDL系列(三)—— SDL2.0 扩展库:SDL_image与SDL_mixer

SDL_image SDL 默认支持的&#xff0c;只能打开 BMP 格式的图片 。 然而我们常见的是 Png jpg 格式的图片&#xff0c;于是我们这节完成 SDL 借用 自带的三方库 &#xff0c;来 完成加载渲染 png 等其他图片格式。 SDL_image 简介 使用 SDL_image &#xff0c;您…

Kubernetes的CNI网络插件介绍

前言 CNI 网络在 Kubernetes 中起着关键作用&#xff0c;它负责管理容器的网络连接、通信和安全&#xff0c;为 K8s 集群中的容器提供了稳定、可靠的网络基础设施。在部署 K8s 集群时&#xff0c;选择合适的CNI&#xff08;容器网络接口&#xff09;插件至关重要。不同的 CNI …

【数据可视化-05】:Plotly数据可视化宝典

一、引言 数据可视化是机器学习流程中不可或缺的一部分。通过图形和图表展示数据&#xff0c;我们可以更直观地理解数据的分布、趋势和关联&#xff0c;从而更有效地进行数据分析、特征工程和模型评估。Plotly是一个功能强大且灵活的数据可视化库&#xff0c;它提供了丰富的图表…

【C++】从零开始构建二叉搜索树

送给大家一句话&#xff1a; 我们始终有选择的自由。选错了&#xff0c;只要真诚的反思&#xff0c;真诚面对&#xff0c;也随时有机会修正错误和选择。 – 《奇迹男孩(电影)》 &#x1f4bb;&#x1f4bb;&#x1f4bb;&#x1f4bb;&#x1f4bb;&#x1f4bb;&#x1f4bb;…

机器学习案例:加州房产价格(四)

参考链接&#xff1a;https://hands1ml.apachecn.org/2/#_12 数据探索和可视化、发现规律 通过之前的工作&#xff0c;你只是快速查看了数据&#xff0c;对要处理的数据有了整体了解&#xff0c;现在的目标是更深的探索数据。 首先&#xff0c;保证你将测试集放在了一旁&…

QT状态机1-三态循环状态机

#include "MainWindow.h" #include "ui_MainWindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)

Java学习48-Java 流(Stream)、文件(File)和IO - 复习章节

1.File类的使用 File类的一个实例对应着磁盘上的文件或文件目录。(必须熟悉)File的实例化(新建一个对象)&#xff0c;常用的方法File类中只有新建&#xff0c;删除&#xff0c;获取路径等方法&#xff0c;不包含读写文件的方法&#xff0c;此时需要使用使用下面说的IO流 IO流…