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

继续Xed编辑器开发第二期:使用Rust从0到1写一个文本编辑器的开发进度,这是第三期的内容:

4.1 逐行清除

在每次刷新之前清除整个屏幕似乎不太理想,最好在重新绘制每行时清除每行。让我们删除 Clear(ClearType::All),而是在我们绘制的每行的末尾使用Clear(ClearType::UntilNewLine)

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('~');//add the followingqueue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();//endif i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}fn refresh_screen(&mut self) -> crossterm::Result<()> {//modifyqueue!(self.editor_contents, cursor::Hide, cursor::MoveTo(0, 0))?;self.draw_rows();queue!(self.editor_contents, cursor::MoveTo(0, 0), cursor::Show)?;self.editor_contents.flush()}
}

4.2 添加版本信息

是时候了,让我们简单地在屏幕下方的三分之一处显示编辑器的名称和版本。

const VERSION: &str = "0.0.1";impl Output{...fn draw_rows(&mut self) {let screen_rows = self.win_size.1;let screen_columns = self.win_size.0; // add this linefor i in 0..screen_rows {// add the followingif i == screen_rows / 3 {let mut welcome = format!("X Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}self.editor_contents.push_str(&welcome);} else {self.editor_contents.push('~');}/* end */queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}
}

我们使用 format!() 宏来加入 VERSION 消息。然后检查长度是否大于屏幕一次可以显示的长度。如果大于,则将其截断。

  • 现在处理下居中
 impl Output {...fn draw_rows(&mut self) {let screen_rows = self.win_size.1;let screen_columns = self.win_size.0;for i in 0..screen_rows {if i == screen_rows / 3 {let mut welcome = format!("Pound Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}/* add the following*/let mut padding = (screen_columns - welcome.len()) / 2;if padding != 0 {self.editor_contents.push('~');padding -= 1}(0..padding).for_each(|_| self.editor_contents.push(' '));self.editor_contents.push_str(&welcome);/* end */} else {self.editor_contents.push('~');}queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}
}

使字符串居中,可以将屏幕宽度除以 2,然后从中减去字符串长度的一半。换言之: screen_columns/2 - welcome.len()/2 ,简化为 (screen_columns - welcome.len()) / 2 。这告诉你应该从屏幕左边缘开始打印字符串的距离。因此,我们用空格字符填充该空间,除了第一个字符,它应该是波浪号


4.3 移动光标

现在让我们转到光标的控制上。目前,箭头键和其他任何键都不能移动游标。让我们从使用 wasd 键移动游标开始。

  • 新建一个CursorController结构体来存储光标信息
struct CursorController {cursor_x: usize,cursor_y: usize,
}impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}
}

cursor_x 是光标的水平坐标(列), cursor_y 是垂直坐标(行)。我们将它们初始化为 0 ,因为我们希望光标从屏幕的左上角开始。

  • 现在让我们向 Output struct and update refresh_screen() 添加一个 cursor_controller 字段以使用 cursor_xcursor_y
struct Output {win_size: (usize, usize),editor_contents: EditorContents,cursor_controller: CursorController, // add this field
}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(),cursor_controller: CursorController::new(), /* add initializer*/}}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;let screen_columns = self.win_size.0;for i in 0..screen_rows {if i == screen_rows / 3 {let mut welcome = format!("Xed Editor --- Version {}", VERSION);if welcome.len() > screen_columns {welcome.truncate(screen_columns)}let mut padding = (screen_columns - welcome.len()) / 2;if padding != 0 {self.editor_contents.push('~');padding -= 1}(0..padding).for_each(|_| self.editor_contents.push(' '));self.editor_contents.push_str(&welcome);} else {self.editor_contents.push('~');}queue!(self.editor_contents,terminal::Clear(ClearType::UntilNewLine)).unwrap();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, cursor::MoveTo(0, 0))?;self.draw_rows();/* modify */let cursor_x = self.cursor_controller.cursor_x;let cursor_y = self.cursor_controller.cursor_y;queue!(self.editor_contents,cursor::MoveTo(cursor_x as u16, cursor_y as u16),cursor::Show)?;/* end */self.editor_contents.flush()}
}
  • 现在我们添加一个 CursorController 方法来控制各种按键的移动逻辑:
impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}/* add this function */fn move_cursor(&mut self, direction: char) {match direction {'w' => {self.cursor_y -= 1;}'a' => {self.cursor_x -= 1;}'s' => {self.cursor_y += 1;}'d' => {self.cursor_x += 1;}_ => unimplemented!(),}}
}

这段逻辑很简单,就不过多解释了。

接下来是修改在Output中使用该方法,因为我们希望通过这个struct于所有的输出都有交互。

impl Output {...fn move_cursor(&mut self,direction:char) {self.cursor_controller.move_cursor(direction);}
}
  • 修改process_keyprocess(),将按下的按键信息传递给move_cursor();
impl Editor {fn new() -> Self {Self {reader: Reader,output: Output::new(),}}fn process_keypress(&mut self) -> crossterm::Result<bool> { /* modify*/match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* add the following*/KeyEvent {code: KeyCode::Char(val @ ('w' | 'a' | 's' | 'd')),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(val),// end_ => {}}Ok(true)}fn run(&mut self) -> crossterm::Result<bool> {self.output.refresh_screen()?;self.process_keypress()}
}
  • 这里使用了@运算符。它的基本作用是创建一个变量并检查该变量是否提供了对于的匹配条件;

  • 因此在这种情况下它创建了val变量,然后检查该变量的取值是否满足给定的四个方向键的字符;

  • 所以,这段逻辑也类似于下面的写法:

     fn process_keypress(&mut self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* note the following*/KeyEvent {code: KeyCode::Char(val),modifiers: KeyModifiers::NONE,}  => {match val {'w'| 'a'|'s'|'d' => self.output.move_cursor(val),_=> {/*do nothing*/}}},// end_ => {}}Ok(true)}
    

现在如果你运行程序并移动光标可能会出现异常终止程序,这是由于溢出导致的OutOfBounds错误,后面会解决。


4.4 使用箭头移动光标

到这里为止,我们已经实现了指定字符按键的移动操作(尽管还有些BUG待修复),接下来就是实现方向键的移动控制功能。

实现上和上面的功能很类似,只需要对原代码进行简单的修改调整即可。

fn process_keypress(&mut self) -> crossterm::Result<bool> {/* modify*/match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),/* modify the following*/KeyEvent {code: direction @ (KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(direction),// end_ => {}}Ok(true)
}
impl CursorController {fn new() -> CursorController {Self {cursor_x: 0,cursor_y: 0,}}/* modify the function*/fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y -= 1;}KeyCode::Left => {self.cursor_x -= 1;}KeyCode::Down => {self.cursor_y += 1;}KeyCode::Right => {self.cursor_x += 1;}_ => unimplemented!(),}}
}
impl Output {...fn move_cursor(&mut self, direction: KeyCode) { //modifyself.cursor_controller.move_cursor(direction);}...
}

4.5 修复光标移动时的越界问题

应该你还记得前面留下的一个BUG,如果记不得了就再去复习一遍,因为即使改用了方向键来移动光标,这个BUG依旧是存在的。

所以这小节主要就是解决这个问题来的。

会出现越界的异常,是因为我们定义的光标坐标的变量cursor_xcursor_y类型是usize,不能为负数。但这一点在我们移动时并不会得到保障,一旦移动导致负数的出现,那么程序就会panic

因为,解决这个问题的手段就是做一下边界判断,将BUG扼杀在摇篮之中。

struct CurSorController {cursor_x: usize,cursor_y: usize,screen_columns:usize,screen_rows:usize,
}
impl CursorController {/* modify */fn new(win_size: (usize, usize)) -> CursorController {Self {cursor_x: 0,cursor_y: 0,screen_columns: win_size.0,screen_rows: win_size.1,}}/* modify the function*/fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y = self.cursor_y.saturating_sub(1);}KeyCode::Left => {if self.cursor_x != 0 {self.cursor_x -= 1;}}KeyCode::Down => {if self.cursor_y != self.screen_rows - 1 {self.cursor_y += 1;}}KeyCode::Right => {if self.cursor_x != self.screen_columns - 1 {self.cursor_x += 1;}}_ => unimplemented!(),}}
}
  1. 向上移动(Up)
    • 使用 saturating_sub 方法来确保不会出现溢出,即当 self.cursor_y 为 0 时,减去 1 后不会变为负数,而是保持为 0。
  2. 向左移动(Left)
    • 如果 self.cursor_x 不等于 0,则将 self.cursor_x 减去 1。
  3. 向下移动(Down)
    • 如果 self.cursor_y 不等于 self.screen_rows - 1,则将 self.cursor_y 加上 1,确保不会超出屏幕的底部。
  4. 向右移动(Right)
    • 如果 self.cursor_x 不等于 self.screen_columns - 1,则将 self.cursor_x 加上 1,确保不会超出屏幕的右侧。
  • 修改 Output struct
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(),cursor_controller: CursorController::new(win_size), /* modify initializer*/}}...}

4.6 翻页和结束

本小节主要是实现上下翻页(快速跳页)以及首页末页的实现;

impl Editor {fn new() -> Self {Self {reader: Reader,output: Output::new(),}}fn process_keypress(&mut self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: KeyModifiers::CONTROL,} => return Ok(false),KeyEvent {code:direction@(KeyCode::Up| KeyCode::Down| KeyCode::Left| KeyCode::Right| KeyCode::Home| KeyCode::End),modifiers: KeyModifiers::NONE,} => self.output.move_cursor(direction),KeyEvent {code: val @ (KeyCode::PageUp | KeyCode::PageDown),modifiers: KeyModifiers::NONE,} =>/*add this */  (0..self.output.win_size.1).for_each(|_| {self.output.move_cursor(if matches!(val, KeyCode::PageUp) {KeyCode::Up} else {KeyCode::Down});}),_ => {}}Ok(true)}fn run(&mut self) -> crossterm::Result<bool> {self.output.refresh_screen()?;self.process_keypress()}
}

如果您使用的是带有 Fn 按键的笔记本电脑,则可以按 Fn+↑ 下并 Fn+↓ 模拟按下 Page UpPage Down 键。

对于HomeEnd的实现也很简单:

fn move_cursor(&mut self, direction: KeyCode) {match direction {KeyCode::Up => {self.cursor_y = self.cursor_y.saturating_sub(1);}KeyCode::Left => {if self.cursor_x != 0 {self.cursor_x -= 1;}}KeyCode::Down => {if self.cursor_y != self.screen_rows - 1 {self.cursor_y += 1;}}KeyCode::Right => {if self.cursor_x != self.screen_columns - 1 {self.cursor_x += 1;}}/* add the following*/KeyCode::End => self.cursor_x = self.screen_columns - 1,KeyCode::Home => self.cursor_x = 0,_ => unimplemented!(),}
}

如果您使用的是带有 Fn 键的笔记本电脑,则可以按 Fn + ←HomeFn + → 模拟按下 和 End 键。

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

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

相关文章

Double 4 VR混合现实情景实训教学系统在戏剧英语课堂上的应用

随着科技的不断发展&#xff0c;Double 4 VR混合现实情景实训教学系统在教育领域的应用越来越广泛。在戏剧英语课堂上&#xff0c;这种教学系统可以为学生提供更加生动、逼真的学习环境&#xff0c;增强学生的学习体验和效果。 一、模拟真实场景&#xff0c;增强学习体验 Doubl…

mybatis关联查询使用resultMap查询到了多条,结果返回一条。

今天在写代码时候&#xff0c;遇到了一个很让我费解的问题&#xff0c;在使用关联查询的时候&#xff0c;在明明数据库里面&#xff0c;已经查到了两条数据&#xff0c;结果resultMap这个集合里面&#xff0c;就只返回一条数据。 数据库的SQL&#xff1a; mybatis的xml里面的r…

cesuim

new Cesium.Color(255,255,0,1), //颜色 Math.PI/2color: Cesium.Color.fromCssColorString("#f40"), //16进制颜色初始化地球 import * as Cesium from "cesium";import { onMounted } from "vue"; onMounted(() > {Cesium.Ion.defaultAcc…

【Python编程实战】基于Python语言实现学生信息管理系统

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

C++的第一道门坎:类与对象(二)

一.类中生成的默认成员函数详解 0.类的6个默认成员函数 编译器会给类生成六个默认成员函数&#xff0c;在类中即使我们什么都不做&#xff0c;也会自动生成。 默认成员函数&#xff1a;用户没有显式实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数。 下面我们逐…

重学java 50 集合 上

路随远&#xff0c;行则将至&#xff1b;路虽难&#xff0c;做则必成 —— 24.5.26 一、Collection接口 1.概述: 单列集合的顶级接口 2.使用 Collection<泛型>对象名new 实现类集合对象<>() <E>:泛型,决定集合中的元素都是啥类型的,必须指定的是引用…

网络渗透day2

Windows登录的明文密码存储过程和密文存储位置 明文密码存储过程&#xff1a; Windows操作系统不会以明文形式存储用户密码。相反&#xff0c;当用户设置或更改密码时&#xff0c;系统会对密码进行哈希处理&#xff0c;然后存储其哈希值。哈希处理的目的是为了提高密码的安全性…

Android性能优化方案

1.启动优化&#xff1a; application中不要做大量耗时操作,如果必须的话&#xff0c;建议异步做耗时操作2.布局优化&#xff1a;使用合理的控件选择&#xff0c;少嵌套。&#xff08;合理使用include,merge,viewStub等使用&#xff09;3.apk优化&#xff08;资源文件优化&#…

在 Vue 中实现表单校验失败后页面滚动到错误处

在 Web 应用程序中&#xff0c;表单验证是保证用户输入的正确性和完整性的关键部分。当用户提交表单时&#xff0c;我们经常需要验证表单数据&#xff0c;并在发现错误时向用户提示。在 Vue.js 中&#xff0c;实现表单验证通常是使用一些流行的库&#xff0c;比如 VeeValidate …

Linux安装Nginx脚本

Hello &#xff0c; 我是恒。 由于有一个脚本网页的仓库&#xff0c;开发必不可少的是自动化安装脚本 该shell脚本是根据某篇文章的脚本改进优化后写出来的&#xff0c;增加了软链接创建和系统服务注册的功能。 同时我也书写了PostgreSQL和Nvm的脚本&#xff0c;请在脚本专栏阅…

Github2024-05-21 Python开源项目日报 Top10

根据Github Trendings的统计,今日(2024-05-21统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目10C项目1TypeScript项目1youtube-dl - 从YouTube和其他网站下载视频的命令行程序 创建周期:4951 天开发语言:Python协议类型:The …

同时安装python2 和python3

最近的项目因为工具的原因 需要同时安装python2 和python3 我又想学着使用python 写东西 导致遇到了很多问题 记录下来 1 同时安装 python2 和python 1.1 安装完把/确认 Path 环境变量里 同时有python2,python2\Scripts和python3 ,python3\Scripts四个环境变量 修改python3…

LabVIEW通过OPC与PLC通讯

使用LabVIEW通过OPC与PLC&#xff08;可编程逻辑控制器&#xff09;通讯是一种常见的工业自动化解决方案。以下是详细的过程介绍&#xff0c;帮助理解并实现该通讯。 1. 理解OPC与PLC OPC&#xff1a;OPC是一种工业通讯协议&#xff0c;旨在实现不同硬件和软件之间的互操作性…

【SpringBoot笔记44】SpringBoot多数据源配置(方式一:配置多个mapper扫描不同的包路径实现多数据源配置)

这篇文章,主要介绍SpringBoot多数据源配置(方式一:配置多个mapper扫描不同的包路径实现多数据源配置)。 目录 一、多数据源配置 1.1、多数据源介绍 1.2、搭建基础工程

AIGC(人工智能生成内容)行业现在适合进入吗

AIGC&#xff08;人工智能生成内容&#xff09;行业现在适合进入吗 发展前景 随着深度学习、自然语言处理&#xff08;NLP&#xff09;、计算机视觉等人工智能技术的飞速发展&#xff0c;AIGC在文本、图像、音频、视频等多个领域都取得了显著的成果&#xff0c;为行业发展提供…

AWS存储之Amazon Aurora

Amazon Aurora是亚马逊自研提供的一种高性能、高可用性的关系型数据库引擎&#xff0c;兼容MySQL和PostgreSQL&#xff0c;提供了与这两种数据库引擎兼容的功能&#xff0c;同时具有更高的性能和可靠性。 Amazon Aurora在全球范围内提供无与伦比的高性能和可用性&#xff0c;完…

3步骤找回丢失文件!EasyRecovery让你轻松应对数据灾难!

EasyRecovery&#xff1a;数据丢失的终结者&#xff0c;您的数字世界守护神 在数字化时代&#xff0c;数据已经成为我们生活的一部分。无论是个人照片、重要文件还是企业资料&#xff0c;数据都扮演着重要的角色。然而&#xff0c;意外删除、格式化、系统崩溃或病毒攻击等原因导…

云原生架构内涵_2.云原生架构原则

云原生架构本身作为一种架构&#xff0c;也有若干架构原则作为应用架构的核心架构控制面&#xff0c;通过遵从这些架构原则可以让技术主管和架构师在做技术选择时不会出现大的偏差。 1.服务化原则 当代码规模超出小团队的合作范围时&#xff0c;就有必要进行服务化拆分了&…

QT的互斥量和信号量

文章目录 一、mutex互斥量1、mutex2、相关成员函数 二、semaphore信号量1、信号量2、成员函数 三、Linux内核中的互斥锁、读写锁、自旋锁、信号量四、QT简单日志类代码 一、mutex互斥量 1、mutex 目的是保护对象、数据结构或代码段&#xff0c;以便一次只有一个线程可以访问它…

云易办springboot+vue后端

springbootvue云易办后端项目完成 一.创建项目 创建父项目&#xff1a;yeb&#xff0c; 使用spring Initializr&#xff0c;完成创建之后删除无用文件夹&#xff0c;作为父项目 添加packaging <packaging>pom</packaging>二.创建子模块&#xff1a;yeb-server …