前言

Vim和Neovim因其独特的模态编辑和高度可定制化,被列为程序员常用的文本编辑器选项之一,与Sublime Text、VS Code、Emacs等编辑器共同丰富了开发者工具生态。就目前而言,网络上绝大多数的文章都在讲解如何为Vim、Neovim编写配置,更深入一点的文章会教大家如何开发相关的插件。但作为开发者的你是否有想过,Vim、Neovim这样amazing的编辑器是如何开发出来的,甚至更大胆一点,你是否有想过能否自己编写一款类Vim编辑器呢?

本系列文章笔者将带你从零开始,基于Vim现有的功能,拆解模态编辑的实现原理,使用Rust一步步开发构建一个具备Vim基本特性的轻量级编辑器。

前置准备

"工欲善其事,必先利其器"。

本系列文章所开发的Vim-like编辑器,将会运行在命令行中,以TUI的形式进行呈现。在代码编写的过程中,我们将会使用Rataui这个三方库来作为最基础的TUI渲染框架。

Ratatui是一个Rust库,用于烹饪美味的TUI(终端用户界面)。它是一个轻量级库,提供了一组小部件和实用程序来构建简单或复杂的Rust TUI。作为TUI构建库,Ratatui也内建了一些基础的 UI 组件,同时社区也有许多基于Ratatui开发的第三方 UI 组件,上手会比较快。

考虑到本文读者不一定都了解这个库,所以笔者在接下来将会简单介绍Ratatui的基本概念和基础使用,让读者对该库有一个直观的感受。同时,在后续文章介绍过程中页会根据实际的情况适当的介绍Ratatui的一些深入的内容,但考虑文章重点,不会过多讲解(不然就成了《从零开始使用Ratatui》了),因此还请笔者自行通过官方的教程学习Ratatui的使用。

ratatui的基本使用

我们首先通过官方的一个基础例子,来理解ratatui的使用思路:

这个例子中最核心部分的莫过于我们在一个loop循环迭代中,先调用终端(terminal)实例的draw方法,完成对 UI 组件的绘制,紧接着读取事件做出对应按键的逻辑响应:

其中的绘制具体流程为:

  1. 执行终端实例的draw方法,传递一个方法回调,该方法入参是一个帧(Frame)实例,代表了每一次绘制的上下文
  2. 调用帧实例的area方法获取终端为当前帧分配的的区域(Rect)
  3. 调用帧实例的draw方法,将 UI 组件绘制到指定区域中

绘制环节完成后,我们调用ratatui::crossterm::event::read()来获取捕获到的事件消息,并针对这些事件的具体分类执行对应逻辑。

读者可能很疑惑,这里的ratatui::crossterm是什么?要解释这个问题,我们要理解一个事实:ratatui是一个TUI渲染引擎,它对实际运行的命令行环境进行了抽象,在内容绘制层面提供了统一的API,而绘制的具体细节逻辑在其内部,通过调用实际的后端(backend)实现,进而调用底层不同的绘制逻辑。当我们调用ratatui::init()时,得到的是一个默认终端实例(DefaultTerminal),该默认终端实例在ratatui内部使用的是crossterm这个底层后端,因此,我们相对应的,需要调用ratatui内置默认依赖的crossterm的相关API来读取事件消息。

ratatui的思维模型

值得介绍的是,ratatui的核心采取的是立即模式渲染(Immediate Mode Rendering),立即模式渲染是一种 UI 范例,其中每帧都会重新创建 UI。与之相对应的是保留模式渲染(Retained Mode Rendering),该模式下通常会创建一组固定的 UI 组件,并在后续的某些时刻更新其状态以达到预期的UI效果。简单来讲:

  • 保留模式(Retained Mode):程序只需进行一次的 UI 组件创建,在随后过程中修改这些组件的其属性或处理组件触发的事件。
  • 即时模式(Immediate Mode):程序根据应用程序状态每帧重新绘制 UI,UI只是对状态的渲染表达,是一个瞬时的对象。

关于这块更多的内容,请读者自行阅读相关的文章进行了解

现在,让我们再通过一个例子来理解立即模式渲染思维。假设我们要在屏幕上渲染一个光标(cursor),这个光标可以随着我们的上下左右按键移动:

对于保留模式渲染来说,我们通常会这样做:

  1. 创建一个光标组件实例
  2. 将光标实例添加到界面上
  3. 响应按键事件,修改光标的位置
let cursor = 创建光标(); // <- 创建“光标”实例
application.屏幕.添加(cursor); // <- 将“光标”添加到屏幕实例上
// ...
// 注册全局按键事件:键盘右方向键按下时,光标位置右移一个单位
application.onRightKeyPress = () => cursor.position.x += 1 application.run(); // 运行程序

而如果是立即渲染模式的架构,cursor仅仅是具有状态的数据,我们每一次循环,都是将光标的状态读取出来,调用应用的绘图API,在对应的位置上画一个“光标”:

let mut cursor = { x: 0, y: 0 }
loop {
draw_at(cursor.x, cursor.y, RED); // <- 每一帧读取光标的位置,在对应位置上绘制
if (存在右方向键按下) {
cursor.x += 1;
}
}

ratatui的进一步实践

为了巩固这块的内容,让我们再实现一个稍微复杂的例子:渲染一个光标,我们可以通过按下空格键来切换是否激活该光标;非激活态的光标呈现白色,且不会响应方向键,激活态光标呈现蓝色,且能够根据方向键进行上下左右移动;无论是否激活,我们总是可以通过按下q键来退出应用。

开发ratatui应用时,最重要的一步是要合理的梳理出应用中所涉及的状态。比如,为了实现上述效果,我们首先梳理出应用所具有的状态:光标的位置(position)、颜色(color)、是否激活(activated)。因此,我们编写如下的Rust结构体来描述上述的状态:

struct Cursor {
x: u16,
y: u16,
activated: bool, // <- 注意,因为光标的颜色与是否激活有关,这里我们简化为只记录是否激活即可
}

接下来,让我们按照三步走:初始化状态;根据状态绘制UI;响应事件修改状态。整体如下:

对于第一步初始化过程很简单:

let mut cursor = Cursor {
x: 0,
y: 0,
activated: false,
};

对于第二步根据状态绘制UI,这里我们在循环中编写如下代码:

terminal.draw(|f: &mut Frame| {
// 先渲染一个灰色背景
f.render_widget(Block::default().bg(Color::Gray), f.area());
// 在对应位置渲染光标
let color = if cursor.activated {
Color::Blue
} else {
Color::White
};
f.buffer_mut()
.cell_mut(Position::new(cursor.x, cursor.y))
.unwrap()
.set_style(Style::default().fg(color).bg(color));
})?;

该过程的具体逻辑为:我们首先调用Frame的render_widget方法在整个命令行界面绘制了一个灰色区块(占满了整个屏幕);接着,根据当前光标的激活状态(activated)来计算出接下来要渲染的光标的颜色;最后,调用Frame的API来获取命令行屏幕缓冲区的某个位置块(Cell),在这个Cell中,我们可以填入了前面计算而来的颜色。

第三步响应事件修改状态的代码如下:

let event = event::read()?;
match event {
Event::Key(KeyEvent { code, .. }) => {
let activated = cursor.activated;
if KeyCode::Char('q') == code {
break Ok(());
} else if KeyCode::Char(' ') == code {
cursor.activated = !activated;
} else if activated {
match code {
KeyCode::Up => cursor.y = cursor.y.saturating_sub(1),
KeyCode::Down => cursor.y = cursor.y.saturating_add(1),
KeyCode::Left => cursor.x = cursor.x.saturating_sub(1),
KeyCode::Right => cursor.x = cursor.x.saturating_add(1),
_ => {}
}
}
}
_ => {}
}

上述代码中,调用的saturating_add、saturating_sub是Rust所提供的安全的加减值操作,避免数字因为其字节长度造成溢出。

在上述代码中,我们通过响应空格键(KeyCode::Char(' '))来切换光标的激活状态;通过响应q键退出循环;通过上下左右按键来控制光标的位置。

至此,我们完成了上述例子的代码编写,运行该应用,我们可以看到对应的效果:

该节实践代码已上传至 github gist:

https://gist.github.com/w4ngzhen/96e49b5054054cab2138c976adf8c98d#file-main-rs

写在最后

本文作为本系列的开篇,还没开始涉及到vim-like编辑器的设计,主要是介绍ratatui这个库的基本的思路概念和一些基本使用。当然,就上述的内容还远远无法胜任接下来的文章,因此笔者希望读者能够仔细阅读官方的文档,掌握ratatui的更多内容,这将有助于更好地理解后续文章内容。

在下一篇文章,笔者将会正式搭建项目,并将详细介绍想要实现一款Vim-like编辑器所必要的一些模型设计。

从零开发Vim-like编辑器(01)起步的更多相关文章

  1. Linux命令行下的vim文本编辑器

    Linux命令行下的vim文本编辑器 下面这个网站的地址讲解的非成分清楚!!!! http://blog.csdn.net/niushuai666/article/details/7275406 学习 ...

  2. linux100day(day3)--常用文本处理命令和vim文本编辑器

    今天,来介绍几个常用文本处理命令和vim文本编辑器 day3--常用文本处理命令和vim文本编辑器 col,用于过滤控制字符,-b过滤掉所有控制字符,这个命令并不常用,但可以使用man 命令名| co ...

  3. 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇)

    系列文章 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇) 手牵手,使用uni-app从零开发一款视频小程序 (系列下 开发实战篇) 前言 好久不见,很久没更新博客了,前段时间 ...

  4. 【原创】新手入门一篇就够:从零开发移动端IM

    一.前言 IM发展至今,已是非常重要的互联网应用形态之一,尤其移动互联网时代,它正以无与论比的优势降低了沟通成本和沟通代价,对各种应用形态产生了深远影响. 做为IM开发者或即将成为IM开发者的技术人员 ...

  5. C#/ASP.NET MVC微信公众号接口开发之从零开发(四) 微信自定义菜单(附源码)

    C#/ASP.NET MVC微信接口开发文章目录: 1.C#/ASP.NET MVC微信公众号接口开发之从零开发(一) 接入微信公众平台 2.C#/ASP.NET MVC微信公众号接口开发之从零开发( ...

  6. C#/ASP.NET MVC微信公众号接口开发之从零开发(二) 接收微信消息并且解析XML(附源码)

    文章导读: C#微信公众号接口开发之从零开发(一) 接入微信公众平台 微信接入之后,微信通过我们接入的地址进行通信,其中的原理是微信用户发送消息给微信公众账号,微信服务器将消息以xml的形式发送到我们 ...

  7. C#/ASP.NET MVC微信公众号接口开发之从零开发(三)回复消息 (附源码)

    C#/ASP.NET MVC微信接口开发文章目录: 1.C#/ASP.NET MVC微信公众号接口开发之从零开发(一) 接入微信公众平台 2.C#/ASP.NET MVC微信公众号接口开发之从零开发( ...

  8. 驱动开发学习笔记. 0.01 配置arm-linux-gcc 交叉编译器

    驱动开发读书笔记. 0.01 配置arm-linux-gcc 交叉编译器 什么是gcc: 就像windows上的VS 工具,用来编译代码,具体请自己搜索相关资料 怎么用PC机的gcc 和 arm-li ...

  9. iOS开发Swift篇(01) 变量&常量&元组

    iOS开发Swift篇(01) 变量&常量&元组 说明: 1)终于要写一写swift了.其实早在14年就已经写了swift的部分博客,无奈时过境迁,此时早已不同往昔了.另外,对于14年 ...

  10. python开发vim插件

    [python开发vim插件] 按如下方式使用python开发vim插件,注意调用时使用的是exec. 但在函数中嵌入python代码更为简便,如下: python如何传递参数给python: 代码头 ...

随机推荐

  1. 万字解析Golang的map实现原理

    0.引言 相信大家对Map这个数据结构都不陌生,像C++的map.Java的HashMap.各个语言的底层实现各有不同,在本篇博客中,我将分享个人对Go的map实现的理解,以及深入源码进行分析,相信耐 ...

  2. 『Plotly实战指南』--绘图初体验

    今天,打算通过绘制一个简单的散点图,来开启我们 Plotly 绘图的初次尝试. 本文目的不是介绍如何绘制散点图,而是通过散点图来介绍Plotly 绘图的基础步骤. 1. 绘制散点图:初探 Plotly ...

  3. MongoDB 简单介绍

    MongoDB介绍 疑问 解答 什么是 MongoDB 一个以 JSON 为数据模型的文档数据库 为什么叫文档数据库? 文档来自于 "JSON Document",并非我们一般理解 ...

  4. [SDR] GNU Radio 系列教程 —— GNU Radio RX PDU (接收据包操作)的基础知识(超全)

    目录 1 接收概述 2 相关块介绍 2.1 相关性估计器(Correlation Estimator) 2.2 多相时钟同步(Polyphase Clock Sync) 2.3 线性均衡器(Linea ...

  5. 自动旋转ROS小车(rviz+urdf+xacro)(附加python操作键盘控制小车运动)

    博客地址:https://www.cnblogs.com/zylyehuo/ 成果图 STEP1 创建工作空间 mkdir -p car_ws/src cd car_ws catkin_make ST ...

  6. Nginx 配置 HTTPS 完整过程

    配置站点使用 https,并且将 http 重定向至 https. 1. nginx 的 ssl 模块安装 查看 nginx 是否安装 http_ssl_module 模块. $ /usr/local ...

  7. 记一次Linux虚拟机分配内存不足的处理方案

    记一次Linux虚拟机硬盘空间不足的处理方案 **起因:**公司的服务器是windows的,而我需要一个基于Linux的dev环境,于是用vmvare创建了一个centos7的系统实例,里面安装mys ...

  8. 接口介绍以及定义和使用--java进阶day02

    1.接口介绍 日常生活中有很多接口,比如手机数据线的接口和手机充电器的接口 我们转换视角,站在设计者的角度思考接口,接口体现出规则,手机的接口大小和数据线的接口大小必须一致,各种接口的大小都要一致,都 ...

  9. 关于ASCII码的一些信息(转载自https://blog.csdn.net/na_tion/article/details/50148883)

    ASCII码分基本表(128个字符,从00000000到01111111).扩展表(256个字符,从00000000到11111111)和压缩表(64个字符),我们经常用的是128个的基本表,而在一些 ...

  10. JavaScript 获取鼠标点击位置坐标(转载自https://www.cnblogs.com/dolphinX/archive/2012/10/09/2717119.html )

    在一些DOM操作中我们经常会跟元素的位置打交道,鼠标交互式一个经常用到的方面,令人失望的是不同的浏览器下会有不同的结果甚至是有的浏览器下没结果,这篇文章就上鼠标点击位置坐标获取做一些简单的总结,没特殊 ...