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

第三篇

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

3.1 使用Ctrl+Q退出

modifiers: event::KeyModifiers::CONTROL,

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


3.2 重构键盘输入

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

  • 首先,让我们创建一个 struct 可以读取各种按键的按钮。我们将其命名为:Reader:
struct Reader;
  • 然后添加一个方法来读取关键事件:
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 ,它将是我们项目的主要主脑。
struct Editor {reader: Reader,
}impl Editor {fn new() -> Self {Self { reader: Reader }}
}

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

  • 现在让我们处理 返回 Reader 的事件并创建一个 run 函数:
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()

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来处理输出相关的内容;

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

  • 修改调用关系:
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(())
}

image-20240515103320326


3.4 重新定位光标

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

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");

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 一样。在我们的文本编辑器中,我们将在正在编辑的文件末尾之后的任何行的开头绘制一个波浪号。

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

  • 现在让我们修改代码以绘制正确数量的波浪号:
/* 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!() 添加一个新行),但这会导致终端滚动以便为新的空白行腾出空间。

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 追加缓冲区

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

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)}
}
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:
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 ,因此我们需要对之前的部分代码做一下调整:

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

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

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

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 thisterminal::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()}
}

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

  • 逐行清除
  • 添加欢迎和版本信息
  • 按键移动光标
  • 方向键移动光标
  • 光标移动溢出问题
  • 分页和首尾页

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

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

相关文章

《Rust奇幻之旅:从Java和C++开启》第1章Hello world 2/5

讲动人的故事,写懂人的代码 很多程序员都在自学Rust。 🤕但Rust的学习曲线是真的陡,让人有点儿怵头。 程序员工作压力大,能用来自学新东西的时间简直就是凤毛麟角。 📕目前,在豆瓣上有7本Rust入门同类书。它们虽有高分评价,但仍存在不足。 首先,就是它们介绍的Rust新…

什么是谷歌爬虫?

其实就是谷歌用来浏览网络信息的一个自动化程序&#xff0c;他们会在你的网站爬取&#xff0c;寻找和搜集信息&#xff0c;谷歌爬虫可以说决定着一个网站在谷歌的生死 谷歌爬虫的作用机制就在于发现新网站以及新网页&#xff0c;然后他会把网页的内容带回去&#xff0c;更新到…

PikaUnsafe upfileupload

1.client check 客户端检测&#xff0c;前端js检测&#xff0c;禁用js和修改后缀名即可。 php格式不能上传&#xff0c;我们修改后缀上传。 蚁剑成功连接。 2.MIME type 这个就是 content-type 规定上传类型&#xff0c;上面的方法也能成功&#xff0c;也可以修改 conten-ty…

面试框架【面试准备】

前言 2023-9-12 12:12:04 2023-09-14 16:13:04 公开发布于 2024-5-22 00:16:21 以下内容源自《【面试准备】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于CSDN平台 作者是CSDN日星月云 博客主页是https://blog.csdn.net/qq_51625007 禁止其他平…

奇偶数递增递减-第13届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第70讲。 奇偶数递增递减&a…

vite+ts+mock+vue-router+pinia实现vue的路由权限

0.权限管理 前端的权限管理主要分为如下&#xff1a; 接口权限路由权限菜单权限按钮权限 权限是对特定资源的访问许可&#xff0c;所谓权限控制&#xff0c;也就是确保用户只能访问到被分配的资源 1.项目搭建 创建vite项目 yarn create vite配置别名 npm install path -…

4. C++入门:内联函数、auto关键字、范围for及nullptr

内联函数 概念 以inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方展开&#xff0c;没有函数调用建立栈帧的开销&#xff0c;内联函数提升程序运行的效率 对比C的宏 C语言不足&#xff1a;宏 #define ADD(x, y) ((x)(y))int main() {int ret…

python实现520表白图案

今天是520哦&#xff0c;作为程序员有必要通过自己的专业知识来向你的爱人表达下你的爱意。那么python中怎么实现绘制520表白图案呢&#xff1f;这里给出方法&#xff1a; 1、使用图形库&#xff08;如turtle&#xff09; 使用turtle模块&#xff0c;你可以绘制各种形状和图案…

Docker 安装kingbase V8r6

下载 官网下载&#xff0c;注意&#xff1a;这里下载 Docker 版本v8r6 安装 # 导入镜像 docker load -i kingbase.tar# 重命名 docker tag [image-name]:[tag] [new-image-name]:[new-tag]# 删除 docker rmi [image-name]:[tag]# 创建容器 docker run -tid \ --privileged \…

【前端】使用 Canvas 实现贪吃蛇小游戏

使用 Canvas 实现贪吃蛇小游戏 在这篇博客中&#xff0c;我们将介绍如何使用 HTML5 Canvas 和 JavaScript 实现一个简单的贪吃蛇&#xff08;Snake&#xff09;小游戏。这个项目是一个基础的游戏开发练习&#xff0c;它可以帮助你理解如何在 Canvas 上绘图、如何处理用户输入以…

Android:OkHttp网络请求框架的使用

目录 一&#xff0c;OkHttp简介 二&#xff0c;OkHttp请求处理流程 三&#xff0c;OkHttp环境配置 四&#xff0c;OkHttp的使用 1.get网络请求 2.post上传表单数据 3.post上传json格式数据 4.文件上传 5.文件下载 一&#xff0c;OkHttp简介 OkHttp是square公司推出的一…

rust的版本问题,安装问题,下载问题

rust的版本、安装、下载问题 rust版本问题&#xff0c; 在使用rust的时候&#xff0c;应用rust的包&#xff0c;有时候包的使用和rust版本有关系。 error: failed to run custom build command for pear_codegen v0.1.2 Caused by: process didnt exit successfully: D:\rus…

Aspose.PDF功能演示:在 JavaScript 中将 TXT 转换为 PDF

您是否正在寻找一种在 JavaScript 项目中将纯文本文件从TXT无缝转换为PDF格式的方法&#xff1f;您来对地方了&#xff01;无论您是要构建 Web 应用程序、创建生产力工具&#xff0c;还是只是希望简化工作流程&#xff0c;直接从 JavaScript 代码中将 TXT 转换为 PDF 的功能都可…

第3天 Web源码拓展_小迪网络安全笔记

1.关于web源码目录结构 #数据库配置文件 后台目录 模板目录 数据库目录 1.1数据库配置文件: 1.1就拿wordpress来说,先到官网下载源码:Download – WordPress.org,解压源码之后: 2.2找到目录下名为 wp-config-sample.php的文件,这就是数据库配置文件: 设想: 我们在渗透…

FOURIER NEURAL OPERATOR FOR PARAMETRIC PARTIAL DIFFERENTIAL EQUATIONS

参数偏微分方程的傅里叶神经算子 论文链接&#xff1a;https://arxiv.org/abs/2010.08895 项目链接&#xff1a;https://github.com/neuraloperator/neuraloperator 作者博客&#xff1a;https://zongyi-li.github.io/blog/2020/fourier-pde/ 参数偏微分方程的傅里叶神经算子…

本地部署Terraria泰拉瑞亚私服并通过内网穿透生成公网地址远程联机

文章目录 前言1. 下载Terraria私服2. 本地运行Terraria 私服3. 本地Terraria私服连接4. Windwos安装Cpolar 工具5. 配置Terraria远程联机地址6. Terraria私服远程联机7. 固定远程联机地址8. 固定的联机地址测试 前言 本文将为你详细介绍在本地如何运行泰拉瑞亚本地私服和结合C…

java企业级云MES系统全套源码,支持app、小程序、H5、台后管理

企业级云MES全套源码&#xff0c;支持app、小程序、H5、台后管理端 企业级智能制造MES系统源码&#xff0c;技术架构&#xff1a;springboot vue-element-plus-admin MES指的是制造企业生产过程执行系统&#xff0c;是一套面向制造企业车间执行层的生产信息化管理系统。MES可以…

栈(基于动态顺序表实现的栈)

栈的简单介绍 关于栈的性质咳咳 栈&#xff1a;栈是一种特殊的线性表,其中只让在一端插入和删除元素。 后进先出 进行插入删除的那一端叫栈顶&#xff0c;另一端叫栈底 我们实现的栈是基于一个动态顺序表的的栈&#xff0c;会实现栈的 入栈&#xff0c;出栈&#xff0c;获取…

修改默认时区,默认语言,默认国家

确认时区&#xff0c;语言&#xff0c;国家 build/make/target/product/languages_default.mkframeworks/base/packages/SettingsLib/res/xml/timezones.xml设备mk中添加相关内容 PRODUCT_PROPERTY_OVERRIDES \persist.sys.timezoneEurope/AmsterdamPRODUCT_PROPERTY_OVERRI…

前端vue用el-table如何实现表头内容过长换行处理,实现换行效果

前端vue用el-table如何实现表头内容过长换行处理&#xff0c;实现换行效果 这是效果图 有两种方法&#xff0c;一种简易版本&#xff0c;一种万能方法,都是el-table&#xff0c;先看文档 表头标题是可以自定义的 方法一 label的解释写在代码里面了&#xff0c;这里会自动形成换…