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 屏幕清理
在用户输入之前将屏幕清理干净,这里使用一个Output
的struct
来处理输出相关的内容;
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(())
}
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
butterminal::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()
}
}
本期完,下期内容抢先知:
- 逐行清除
- 添加欢迎和版本信息
- 按键移动光标
- 方向键移动光标
- 光标移动溢出问题
- 分页和首尾页
更多推荐
所有评论(0)