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;
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

后端之路第一站——Maven

前提&#xff1a;得会基础java 前言&#xff1a;不知道出于什么原因&#xff0c;可能是喜欢犯贱吧&#xff0c;本人从大一到大二都一直在专研前端开发&#xff0c;一点也没接触过后端&#xff0c;但是突然抽风想学后端了&#xff0c;想试着自己全栈搞一下项目&#xff0c;于是在…

为什么公司偏爱高薪招新人,老员工的我怎么办?

目录 前言 性价比 薪酬体系 心理学 技术迭代 老员工的价值 总结 前言 在当下的互联网行业&#xff0c;人才流动性极高&#xff0c;不少公司面临着一个棘手的问题&#xff1a;为什么宁愿花高薪聘请一名应届生&#xff0c;也不愿意给予现有老员工加薪以留住他们&#xff1…

Git大文件无法直接push用git lfs track 上传大文件具体操作

Git 因为大文件push失败 回退到git add前用git lfs track单独添加大文件 以下work flow仅代表个人解决问题的办法&#xff0c;有优化流程的欢迎交流 回退到git add前 以下指令回退一个commit git reset --soft HEAD~1以下指令撤销所有git add操作&#xff0c;但不删除本地修…

数据结构与算法学习笔记三---栈和队列

目录 前言 一、栈 1.栈的表示和实现 1.栈的顺序存储表示和实现 1.C语言实现 2.C实现 2.栈的链式存储表示和实现 1.C语言实现 2.C实现 2.栈的应用 1.数制转换 二、队列 1.栈队列的表示和实现 1.顺序队列的表示和实现 2.链队列的表示和实现 2.循环队列 前言 这篇文…

乡村振兴与农村基础设施建设:加大投入力度,提升建设水平,完善农村基础设施网络,打造宜居宜业的美丽乡村

一、引言 乡村振兴战略是我国在新时代推进农业农村现代化的重大战略部署&#xff0c;其核心目标是实现乡村的全面振兴&#xff0c;促进农业强、农村美、农民富。农村基础设施建设作为乡村振兴的基石&#xff0c;其建设水平直接关系到乡村经济的持续健康发展、乡村环境的改善以…

微软宣布GPT-4o模型,可在 Azure OpenAI上使用

5月14日&#xff0c;微软在官网宣布&#xff0c;OpenAI最新发布的多模态模型GPT-4o&#xff0c;可以在 Azure OpenAI 云服务中使用。 据悉&#xff0c;GPT-4o支持跨文本、视频、音频多模态推理&#xff0c;例如&#xff0c;通过GPT-4o打造一个AI助手&#xff0c;用于辅导孩子解…

【ORACLE战报】2024.4月最新OCP考试喜报.

课程介绍 DBA数据库管理必备认证&#xff1a;ORACLE OCP 19C 教材下载 ORACLE OCP 19C 官方电子教材 ORACLE OCP 12C官方电子教材 题库下载 ORACLE 19C题库 &#xff08;083384题、082362题&#xff09;-2024答案修正版.rar 所有的收获都是默默耕耘的成果 2024.4月【最新考试成…

数据挖掘流程是怎样的?数据挖掘平台基本功能有哪些?

数据挖掘是从大量的、不完全的、有噪声的、模糊的、随机的数据中提取隐含在其中的、人们事先不知道的、但又是潜在有用的信息和知识的过程。 数据挖掘的流程是&#xff1a; 清晰地定义出业务问题&#xff0c;确定数据挖掘的目的。 数据准备: 数据准备包括&am…

精酿啤酒:品质与口感的完善结合

在啤酒的世界中&#xff0c;Fendi club啤酒以其卓着的品质和与众不同的口感赢得了广泛的赞誉。作为精酿啤酒的品牌&#xff0c;Fendi club啤酒始终坚持对品质的追求&#xff0c;为消费者带来超卓的口感体验。 Fendi club啤酒的品质源于对原料的严格挑选和加工。他们选用上好的…

文献速递:多模态深度学习在医疗中的应用--多模式深度学习实现的全癌症整合组织学-基因组学分析

Title 题目 Pan-cancer integrative histology-genomic analysis via multimodal deep learning 多模式深度学习实现的全癌症整合组织学-基因组学分析 01 文献速递介绍 癌症的定义包括肿瘤和组织微环境中标志性的组织病理学、基因组学和转录组学的异质性&#xff0c;这些异…

【数据分析面试】44.分析零售客户群体(Python 集合Set的用法)

题目 假设你是一家在线零售商的数据库管理员&#xff0c;需要分析两类客户的数据。一个集合 purchased_customers 包含在最近一次促销活动中购买了商品的客户ID&#xff0c;另一个集合 newsletter_subscribers 包含订阅了新闻通讯的客户ID。编写一个函数 analyze_customers&am…

C++类与对象基础探秘系列(三)

目录 再谈构造函数 构造函数体赋值 初始化列表 explicit关键字 static成员 概念 特性 友元 友元函数 友元类 内部类 概念 特性 匿名对象 再次理解类和对象 再谈构造函数 构造函数体赋值 在创建对象时&#xff0c;编译器会通过调用构造函数&#xff0c;给对象中的各个成员…

Echarts使用

介绍 ECharts 是一个强大的&#xff0c;基于 JavaScript 的开源数据可视化库&#xff0c;适用于创建多种类型的图表&#xff0c;满足广泛的业务需求。它由百度团队开发并维护&#xff0c;后来捐赠给了 Apache 软件基金会&#xff0c;并已在2021年从孵化项目毕业&#xff0c;成…

【刷题(2)】矩阵

一、矩阵问题基础 遍历: for i in range(len(matrix)): for j in range(len(matrix[0]): while 倒序遍历: for i in range(right,left,-1) 临时存储:temp w,h:len(matrix[0])-1 len(matrix)-1 left,right,top,bottom:0 len(matrix[0])-1 0 len(matrix)-1 索引: width = le…

2024最新互联网公司工作时长排行榜出炉!

“工作时长”&#xff0c;是选择公司的一个非常重要的参考指标。 我们在选择一个公司的时候&#xff0c;除了需要关注总收入package 以外&#xff0c;还需要考虑这家公司的加班时长是否人性化。 我们的工作时长是周工作小时数。法定工作时间是40小时(955)。大小周通常折算为周…

企业大模型如何成为自己数据的“百科全书”?

作者 | 郭炜 编辑 | Debra Chen 在当今的商业环境中&#xff0c;大数据的管理和应用已经成为企业决策和运营的核心组成部分。然而&#xff0c;随着数据量的爆炸性增长&#xff0c;如何有效利用这些数据成为了一个普遍的挑战。 本文将探讨大数据架构、大模型的集成&#xff0…

数据结构篇3—《龙门客“栈”》

文章目录 &#x1f6a9;前言1、栈的概念2、栈的实现框架3、栈的代码实现3.1、栈的初始化和销毁3.2、入栈\出栈\返回栈顶元素\元素个数\判空3.3、栈定义注意事项 4、栈的应用实例——《括号匹配问题》 &#x1f6a9;前言 前面记录了关于顺序表和链表的数据结构&#xff0c;这一篇…

容器安全在云原生的安全上有什么大作为

进入后云计算时代&#xff0c;云原生正在成为企业数字化转型的潮流和加速器。云原生安全相关的公司雨后春笋般建立起来&#xff0c;各个大云厂商也积极建立自己云原生的安全能力&#xff0c;保护云上客户的资产。 与之相对的&#xff0c;黑产组织为了牟利&#xff0c;也在不断…

低功耗设计

设计电路谁都会&#xff0c;但是设计低功耗电路&#xff0c;降低芯片功耗却是难题 - 哔哩哔哩 (bilibili.com) 一个产品的低功耗设计&#xff0c;并不仅仅只是采用一个低功耗的MCU就能解决的问题。产品的低功耗&#xff0c;不久取决于MCU的低功耗&#xff0c;也取决于低功耗的…

QT状态机4-使用并行状态来避免组合爆炸

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