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

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

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

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

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

相关文章

php部分特性漏洞学习

php部分函数漏洞学习 简单总结一些我遇到的ctf中的php的一些函数或特性的漏洞&#xff0c;我刷题还是太少了&#xff0c;所以很多例子来自ctfshow&#xff0c;以后遇到相关赛题再更新 1.MD5和其他hash 弱类型比较 php中&#xff0c;有两中判断相等的符号&#xff0c;和&…

flutter使用dbus插件时,在终端无法使用“dart-dbus”命令

不用flutter的人&#xff0c;可能都不会找到这儿&#xff0c;遇到这个问题&#xff0c;所以这里默认flutter已经装过了&#xff0c;且对flutter如何使用插件也有所了解了。 由于我在项目中用到了dbus插件&#xff0c;用法如图所示&#xff0c;我需要使用这条命令来生成一个sou…

前端javascript包管理,npm升级用pnpm

一 pnpm 介绍 pnpm&#xff08;Package Manager&#xff09;是一个快速、节省磁盘空间的 JavaScript 包管理器&#xff0c;它是 Node.js 生态系统中 npm 的一个替代品。pnpm 解决了传统包管理工具在处理依赖时的一些痛点&#xff0c;特别是关于存储空间使用和依赖地狱的问题。…

vite vue-cli 之vue3安装Vue devtools调试工具

一.vite的官方配置-vite-plugin-vue-devtools vite.config.js import { fileURLToPath, URL } from node:urlimport vue from vitejs/plugin-vue import vueJsx from vitejs/plugin-vue-jsx import { defineConfig } from vite import VueDevTools from vite-plugin-vue-devt…

Android JetPack ViewModel

一、什么是ViewModel&#xff1f; Android ViewModel在我们使用MVVM开发模式时会经常用到&#xff0c;对我来说就是好用&#xff0c;好维护。 它相对于MVC模式&#xff0c; 一来可以减少Activity层的代码&#xff0c;可以把一些业务逻辑和对数据的交互到ViewModel层去&#…

Excel模板计算得出表格看板

背景 表格看板及导出&#xff0c;单元格时间年是根据筛选器时间变化的 较往年和往年是计算单元格 思路 1.通过excel模板来把数据填入excel再数据清洗得到数据返回前端 2.数据填充&#xff0c;通过行列作为key 列如&#xff1a;key整体20241月&#xff0c;根据key匹配数据填…

计算机操作系统总结(1)

1操作系统的概念&#xff08;定义&#xff09;功能和目标 (1)什么是操作系统&#xff1f; &#xff08;2&#xff09;操作系统的功能和目标—作为系统资源的管理者 &#xff08;3&#xff09;操作系统的功能和目标—向上层提供方便易用的服务 &#xff08;4&#xff09;操作系…

中银基金软件开发工程师春招群面记录

本文介绍2024届春招中&#xff0c;中国银行下属中银基金管理有限公司的软件开发工程师岗位1场面试的基本情况、提问问题等。 2024年04月投递了中国银行的共计4个部门或单位&#xff0c;包括中银基金管理有限公司的软件开发工程师岗位&#xff0c;暂时不清楚所在部门。目前完成了…

平均分配问题

将dwd_phone_shop这六行数据按照1 3 5,2 4 6的排序分为两组,与dwd_phone_base连接,分别与为其中1 3 5 为shop_Id为1的那一组 2 4 6 为shop_Id为2 的那一组 取余法 开窗函数法

南加州大学字节提出MagicPose,提供逼真的人类视频生成,实现生动的运动和面部表情传输,以及不需要任何微调的一致的野外零镜头生成。

MagicPose可以精确地生成外观一致的结果&#xff0c;而原始的文本到图像模型(如Stable Diffusion和ControlNet)很难准确地保持主体身份信息。 此外&#xff0c;MagicPose模块可以被视为原始文本到图像模型的扩展/插件&#xff0c;而无需修改其预训练的权重。 相关链接 论文链…

javaSwing飞机订票系统

摘要 Java swing实现的飞机票预定系统&#xff0c;系统数据库原本采用的是Oracle&#xff0c;我又改了一个mysql版本的&#xff0c;所以这套系统有两个版本&#xff0c;一个是mysql数据库版的&#xff0c;一个是Oracle数据库版 一&#xff0e; 已经完成的功能 &#xff1a; …

Linux基础命令[27]-gpasswd

文章目录 1. gpasswd 命令说明2. gpasswd 命令语法3. gpasswd 命令示例3.1 不加参数3.2 -a&#xff08;将用户加入组&#xff09;3.3 -d&#xff08;从组中删除用户&#xff09;3.4 -r&#xff08;删除组密码&#xff09;3.5 -M&#xff08;多个用户一起加入组&#xff09;3.6 …

excel里如何将数据分组转置?

这个表格怎样转换为下表&#xff1f;按照国家来分组&#xff0c;把不同年份对应的不同序列值进行转置&#xff1f;&#xff1f; 这演示用数据透视表就完成这个数据转换。 1.创建数据透视表 选中数据中任意单元格&#xff0c;点击插入选项卡&#xff0c;数据透视表&#xff0c;…

C++设计模式|结构型 适配器模式

1.什么是适配器模式&#xff1f; 可以将⼀个类的接⼝转换成客户希望的另⼀个接⼝&#xff0c;主要⽬的是 充当两个不同接⼝之间的桥梁&#xff0c;使得原本接⼝不兼容的类能够⼀起⼯作。 2. 适配器模式的组成 &#xff08;1&#xff09;接口类&#xff0c;给客户端调用&…

vue打包部署到springboot,通过tomcat运行

tomcat默认端口 8080springboot端口 9132vue 端口 9131 框架 项目是基于SpringBootVue前后端分离的仓库管理系统 后端&#xff1a;SpringBoot MybatisPlus前端&#xff1a;Node.js Vue element-ui数据库&#xff1a;mysql 一. 打包Vue项目 cmd中输入命令 npm run build 后…

PHP在线制作表白网源码

PHP在线制作表白网源码&#xff0c;送女友个惊喜吧&#xff0c;无数据库&#xff0c;上传就能用&#xff0c;后台/admin&#xff0c;账号密码都是admin 百度网盘&#xff1a;https://pan.baidu.com/s/1rbD2_8IsP9UPLK-cdgEXfA?pwdre59

前端绘制流程节点数据

根据数据结构和节点的层级、子节点id&#xff0c;前端自己绘制节点位置和关联关系、指向、已完成节点等 <template><div><div>通过后端节点和层级&#xff0c;绘制出节点以及关联关系等</div><div class"container" ref"container&…

本地centos7+docker+ollama+gpu部署

1、一台有 NVIDIA GPU 驱动的机器 2、Docker CE安装 # 删除旧版本的 Docker&#xff08;如果存在&#xff09; sudo yum remove -y docker docker-common docker-selinux docker-engine # 安装必要的软件包&#xff1a; sudo yum install -y yum-utils device-mapper-persiste…

springboot3项目练习详细步骤(第四部分:文件上传、登录优化、多环境开发)

目录 本地文件上传 接口文档 业务实现 登录优化 SpringBoot集成redis 实现令牌主动失效机制 多环境开发 本地文件上传 接口文档 业务实现 创建FileUploadController类并编写请求方法 RestController public class FileUploadController {PostMapping("/upload&…

EPBU/MOBI转PDF

--痛苦 --不爱BB 直接上码。 写了一个java方法&#xff0c;转epub 或者mobi 为 pdf的方法 &#xff08;单个转换&#xff09; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader;public class EbookConvert…