本文最后更新于:2024年1月10日 下午
插入普通字符
现在我们编写一个insert
函数,在给定位置向一个Document
中插入单个字符。我们先从允许在字符串的给定位置添加一个字符开始。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @@ -1,4 +1,5 @@ use crate::Row; +use crate::Position; use std::fs; #[derive(Default)] @@ -28,4 +29,15 @@ impl Document { pub fn len(&self) -> usize { self.rows.len() } + + pub fn insert(&mut self, at: &Position, c: char) { + if at.y == self.len() { + let mut row = Row::default(); + row.insert(0, c); + self.rows.push(row); + } else if at.y < self.len() { + let row = self.rows.get_mut(at.y).unwrap(); + row.insert(at.x, c); + } + } }
|
src/editor.rs1 2 3 4 5 6 7 8
| @@ -104,6 +104,7 @@ impl Editor { let pressed_key = Terminal::read_key()?; match pressed_key { Key::Ctrl('q') => self.should_quit = true, + Key::Char(c) => self.document.insert(&self.cursor_position, c), Key::Up | Key::Down | Key::Left
|
src/row.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @@ -1,6 +1,7 @@ use std::cmp; use unicode_segmentation::UnicodeSegmentation; +#[derive(Default)] pub struct Row { string: String, len: usize, @@ -44,4 +45,17 @@ impl Row { pub fn update_len(&mut self) { self.len = self.string[..].graphemes(true).count(); } + + pub fn insert(&mut self, at: usize, c: char) { + if at >= self.len() { + self.string.push(c); + } else { + let mut resule: String = self.string[..].graphemes(true).take(at).collect(); + let remainder: String = self.string[..].graphemes(true).skip(at).collect(); + resule.push(c); + resule.push_str(&remainder); + self.string = resule; + } + self.update_len(); + } }
|
在Row
中。我们分为两种情况进行处理:如果我们恰好位于字符串的末尾,就将字符追加入其中。这可能发生在用户位于一行的末尾并且继续输入时。反之,我们通过逐个字符遍历来重建我们的字符串。
我们使用迭代器的take
和skip
函数来创建新的迭代器,一个从0
开始到at
(包括at
),另一个从at
之后的元素开始到末尾。我们使用collect
来将这些迭代器合并成字符串。由于collect
可以创建多种集合,我们必须提供result
和remainder
的类型,否则Rust
不会知道要创建什么类型的集合。
与我们在Row
中做的类似,我们正在处理用户试图在文档底部插入的情况,这种情况下我们将创建一个新行。
现在我们需要在输入字符时调用该方法。我们通过扩展editor
中的process_keypress
来做到这一点。
有了这个更改,我们现在可以在文档的任何地方添加字符。但我们的光标不会移动—所以我们实际上是在反向输入文字。让我们通过将“输入一个字符”视为“在光标右侧输入一个字符”来解决这个问题。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12
| @@ -104,7 +104,10 @@ impl Editor { let pressed_key = Terminal::read_key()?; match pressed_key { Key::Ctrl('q') => self.should_quit = true, - Key::Char(c) => self.document.insert(&self.cursor_position, c), + Key::Char(c) => { + self.document.insert(&self.cursor_position, c); + self.move_cursor(Key::Right); + } Key::Up | Key::Down | Key::Left
|
行内删除
我们现在想实现Backspace
和Delete
的功能。先从Delete
键开始,它应该删除光标下的字符。如果你的光标是一个线条|
,而不是一个块▪️
,“光标下” 意味着 “光标前面”,因为光标是一个在其位置左侧显示的闪烁线。让我们添加一个delete
函数。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @@ -40,4 +41,13 @@ impl Document { row.insert(at.x, c); } } + + pub fn delete(&mut self, at: &Position) { + if at.y >= self.len() { + return; + } else { + let row = self.rows.get_mut(at.y).unwrap(); + row.delete(at.x); + } + } }
|
src/editor.rs1 2 3 4 5 6 7 8
| @@ -108,6 +108,7 @@ impl Editor { self.document.insert(&self.cursor_position, c); self.move_cursor(Key::Right); } + Key::Delete => self.document.delete(&self.cursor_position), Key::Up | Key::Down | Key::Left
|
src/row.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @@ -58,4 +58,16 @@ impl Row { } self.update_len(); } + + pub fn delete(&mut self, at: usize) { + if at >= self.len() { + return; + } else { + let mut result: String = self.string[..].graphemes(true).take(at).collect(); + let remainder: String = self.string[..].graphemes(true).skip(at + 1).collect(); + result.push_str(&remainder); + self.string = result; + } + self.update_len(); + } }
|
这段代码和我们之前编写的insert
代码非常相似。不同之处在于,在Row
中,我们不是在添加一个字符,而是在合并结果result
和剩余部分remainder
时跳过我们想要删除的那个字符。在Document
中,我们目前还没有实现删除一行的情况。
接下来让我们处理Backspace
。从本质上讲,Backspace
是向左移动和删除的结合,因此我们按如下方式调整process_keypress
:
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13
| @@ -109,6 +109,12 @@ impl Editor { self.move_cursor(Key::Right); } Key::Delete => self.document.delete(&self.cursor_position), + Key::Backspace => { + if self.cursor_position.x > 0 || self.cursor_position.y > 0 { + self.move_cursor(Key::Left); + self.document.delete(&self.cursor_position); + } + } Key::Up | Key::Down | Key::Left
|
Backspace
现在可以在行内工作了。我们还确保了如果我们位于文档的开头,不会执行删除操作,否则,我们将开始删除光标左侧的元素。
复杂删除
目前我们有两个特殊情况尚未处理:一个是在行的开头使用Backspace
,另一个是在行的末尾使用Delete
。在我们的实现中,Backspace
简单地向左移动,这在行的开头意味着移动到前一行的末尾,然后尝试删除一个字符。这意味着,一旦我们允许在行末进行删除操作,Backspace
的情况就会得到解决。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @@ -42,8 +42,14 @@ impl Document { } pub fn delete(&mut self, at: &Position) { - if at.y >= self.len() { + let len = self.len(); + if at.y >= len { return; + } + if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 { + let next_row = self.rows.remove(at.y + 1); + let row = self.rows.get_mut(at.y).unwrap(); + row.append(&next_row); } else { let row = self.rows.get_mut(at.y).unwrap(); row.delete(at.x);
|
src/row.rs1 2 3 4 5 6 7 8 9 10
| @@ -70,4 +70,9 @@ impl Row { } self.update_len(); } + + pub fn append(&mut self, new_row: &Self) { + self.string = format!("{}{}", self.string, new_row.string); + self.update_len(); + } }
|
我们在Row中增加了一个新方法append
,可以把另一个row
的内容追加到末尾。我们在Document
中使用这个功能。现在,Document
中的代码看起来有点复杂。它所做的是检查我们是否位于一行的末尾,以及是否有另一行跟在这一行之后。如果是这样,我们就从我们的vec
中移除下一行并将其追加到当前行。反之,我们就尝试从当前行中删除。
那么,为什么代码看起来这么复杂呢?我们能否简单地将row
的定义移动到 if 语句之上,以使事情更清楚些?
由于Rust
的租用的概念,我们不能同时对向量内的元素持有两个可变引用,也不能在对向量内的一个元素持有可变引用时修改向量。为什么?因为比方说我们有一个包含[A, B, C]
的向量,我们持有一个指向B
内存位置的引用。现在我们移除A
,导致B
和C
向左移动。我们的引用会突然不再指向B
,而是指向C
。
这意味着我们不能持有对row
的引用然后删除向量的一部分。所以我们首先直接读取row
的长度,而不保留引用。然后我们通过从中移除一个元素来改变向量,接着我们创建对row
的可变引用。
ENTER键
最后我们来实现Enter
。Enter
键允许用户在文本中插入新行或将一行分割成两行。你现在确实可以通过这种方式添加新行,但正如你可能预料的,处理方式并不理想。这是因为新行被作为行的一部分插入,而不是创建新的行。
让我们从一个简单的情况开始,即在当前行下方添加一个新行。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @@ -30,7 +30,23 @@ impl Document { self.rows.len() } + fn insert_new_line(&mut self, at: &Position) { + if at.y > self.len() { + return; + } + let new_row = Row::default(); + if at.y == self.len() || at.y.saturating_add(1) == self.len() { + self.rows.push(new_row); + } else { + self.rows.insert(at.y + 1, new_row); + } + } + pub fn insert(&mut self, at: &Position, c: char) { + if c == '\n' { + self.insert_new_line(at); + return; + } if at.y == self.len() { let mut row = Row::default(); row.insert(0, c);
|
我们在有新行传入时从insert
调用insert_new_line
函数。在insert_new_line
中,我们会检查Enter
键是否在文档的最后一行或其下一行被按下。如果是这种情况,我们会在我们的vec
的末尾推入一个新行。如果不是这种情况,我们会在正确的位置插入一个新行。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @@ -34,10 +34,11 @@ impl Document { if at.y > self.len() { return; } - let new_row = Row::default(); - if at.y == self.len() || at.y.saturating_add(1) == self.len() { - self.rows.push(new_row); + if at.y == self.len() { + self.rows.push(Row::default()); + return; } else { + let new_row = self.rows.get_mut(at.y).unwrap().split(at.x); self.rows.insert(at.y + 1, new_row); } }
|
src/row.rs1 2 3 4 5 6 7 8 9 10 11 12 13
| @@ -75,4 +75,12 @@ impl Row { self.string = format!("{}{}", self.string, new_row.string); self.update_len(); } + + pub fn split(&mut self, at: usize) -> Self { + let beginning: String = self.string[..].graphemes(true).take(at).collect(); + let remainder: String = self.string[..].graphemes(true).skip(at).collect(); + self.string = beginning; + self.update_len(); + Self::from(&remainder[..]) + } }
|
我们现在添加了一个叫做split
的方法,它会截断当前行直到给定的索引,并返回包含该索引之后所有内容的另一行。在Document
上,如果我们位于最后一行之下,我们现在只推入一个空行;在其他任何情况下,我们都是用split
改变当前行并插入结果。即使在一行的末尾,这也是有效的,在这种情况下新行将只包含一个空字符串。
现在我们可以在文档中自由移动,添加空白、字符,甚至是表情符号,删除行等等。
保存文件
现在我们终于使文本变得可编辑了,让我们实现将其保存到磁盘上的功能。我们从在Document
中实现一个save
方法开始。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @@ -1,6 +1,7 @@ use crate::Position; use crate::Row; use std::fs; +use std::io::{Error, Write}; #[derive(Default)] pub struct Document { @@ -72,4 +73,15 @@ impl Document { row.delete(at.x); } } + + pub fn save(&self) -> Result<(), Error> { + if let Some(file_name) = &self.file_name { + let mut file = fs::File::create(file_name)?; + for row in &self.rows { + file.write_all(row.as_bytes())?; + file.write_all(b"\n")?; + } + } + Ok(()) + } }
|
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @@ -56,7 +56,7 @@ impl Editor { pub fn default() -> Self { let args: Vec<String> = env::args().collect(); - let mut initial_status = String::from("HELP: Ctrl-Q = quit"); + let mut initial_status = String::from("HELP: Ctrl-S = save | Ctrl-Q = quit"); let document = if args.len() > 1 { let file_name = &args[1]; let doc = Document::open(&file_name); @@ -104,6 +104,14 @@ impl Editor { let pressed_key = Terminal::read_key()?; match pressed_key { Key::Ctrl('q') => self.should_quit = true, + Key::Ctrl('s') => { + if self.document.save().is_ok() { + self.status_message = + StatusMessage::form("File saved successfully".to_string()); + } else { + self.status_message = StatusMessage::form("Error saving file".to_string()); + } + } Key::Char(c) => { self.document.insert(&self.cursor_position, c); self.move_cursor(Key::Right);
|
src/row.rs1 2 3 4 5 6 7 8 9
| @@ -83,4 +83,8 @@ impl Row { self.update_len(); Self::from(&remainder[..]) } + + pub fn as_bytes(&self) -> &[u8] { + self.string.as_bytes() + } }
|
我们扩展了Row
,增加了一个方法,允许我们将行转换为字节数组。在Document
中,write_all
方法接收这个字节数组并将其写入到磁盘。由于我们的行不包含新行的标志,我们需要单独写出它。b
前缀表示这是一个字节数组,而不是字符串。
由于写入可能会引发错误,我们的save
函数返回一个Result
类型,并且我们使用?
操作符来传递可能发生的任何错误给调用者。
在Document
中,我们将save
方法与Ctrl-S
绑定。我们通过使用is_ok
方法来检查写入是否成功,它在Result
是Ok
而不是Err
时返回true
,然后我们相应地设置状态消息。
另存为…
当前,当用户不带任何参数运行iTEditor
时,会得到一个空白文件进行编辑,但没有办法保存。我先现在创建一个prompt()
方法,它可以在状态栏显示一个提示符,并允许用户在该提示符后输入一行文本。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @@ -105,6 +105,9 @@ impl Editor { match pressed_key { Key::Ctrl('q') => self.should_quit = true, Key::Ctrl('s') => { + if self.document.file_name.is_none() { + self.document.file_name = Some(self.prompt("Save as: ")?); + } if self.document.save().is_ok() { self.status_message = StatusMessage::form("File saved successfully".to_string()); @@ -286,6 +289,24 @@ impl Editor { print!("{}", text); } } + + fn prompt(&mut self, prompt: &str) -> Result<String, std::io::Error> { + let mut result = String::new(); + loop { + self.status_message = StatusMessage::form(format!("{}{}", prompt, result)); + self.refresh_screen()?; + if let Key::Char(c) = Terminal::read_key()? { + if c == '\n' { + self.status_message = StatusMessage::form(String::new()); + break; + } + if !c.is_control() { + result.push(c); + } + } + } + Ok(result) + } }
|
用户的输入会被存储在一个名为result
的变量中,我们将其初始化为一个空字符串。接着,程序将进入一个无限循环,循环中不断设置状态消息,刷新屏幕,并等待处理按键输入。当用户按下Enter
时,状态消息会被清除,并返回输入的消息。途中可能出现的错误会被向上传播。
现在用户可以保存文件了,我们在提示功能中再处理几个情况。现在让我们允许用户通过按Esc
和使用Backspace
,同时也将空输入视为取消操作。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| @@ -100,21 +100,28 @@ impl Editor { Terminal::flush() } + fn save(&mut self) { + if self.document.file_name.is_none() { + let new_name = self.prompt("Save as: ").unwrap_or(None); + if new_name.is_none() { + self.status_message = StatusMessage::form("Save aborted".to_string()); + return; + } + self.document.file_name = new_name; + } + + if self.document.save().is_ok() { + self.status_message = StatusMessage::form("File saved successfully.".to_string()); + } else { + self.status_message = StatusMessage::form("Error writing file!".to_string()); + } + } + fn process_keypress(&mut self) -> Result<(), std::io::Error> { let pressed_key = Terminal::read_key()?; match pressed_key { Key::Ctrl('q') => self.should_quit = true, - Key::Ctrl('s') => { - if self.document.file_name.is_none() { - self.document.file_name = Some(self.prompt("Save as: ")?); - } - if self.document.save().is_ok() { - self.status_message = - StatusMessage::form("File saved successfully".to_string()); - } else { - self.status_message = StatusMessage::form("Error saving file".to_string()); - } - } + Key::Ctrl('s') => self.save(), Key::Char(c) => { self.document.insert(&self.cursor_position, c); self.move_cursor(Key::Right); @@ -290,22 +297,35 @@ impl Editor { } } - fn prompt(&mut self, prompt: &str) -> Result<String, std::io::Error> { + fn prompt(&mut self, prompt: &str) -> Result<Option<String>, std::io::Error> { let mut result = String::new(); loop { self.status_message = StatusMessage::form(format!("{}{}", prompt, result)); self.refresh_screen()?; - if let Key::Char(c) = Terminal::read_key()? { - if c == '\n' { - self.status_message = StatusMessage::form(String::new()); - break; + match Terminal::read_key()? { + Key::Backspace => { + if !result.is_empty() { + result.truncate(result.len() - 1); + } } - if !c.is_control() { - result.push(c); + Key::Char('\n') => break, + Key::Char(c) => { + if !c.is_control() { + result.push(c); + } + } + Key::Esc => { + result.truncate(0); + break; } + _ => (), } } - Ok(result) + self.status_message = StatusMessage::form(String::new()); + if result.is_empty() { + return Ok(None); + } + Ok(Some(result)) } }
|
现在的prompt
不仅包含了一个Result
类型,还包含了一个Option
类型。这样做的想法是,即使提示操作成功,它也可以返回None
,表示用户已经中断了提示。我们用match
替换了原来的if
语句,以便也能处理退格和Esc
情况。在Esc
的情况下,我们将重置所有之前输入的文本,并退出循环。在退格的情况下,我们将逐个减少输入,移除正在进行中的最后一个字符。
最后,我们在process_keypress
外部创建了一个save
函数。在这个函数中,如果提示返回了None
或者返回了错误,我们现在都将中断保存操作。
修改标志
我们希望跟踪我们编辑器中加载的文本与文件中的内容是否有差异。然后我们可以警告用户,当他们尝试退出时可能会丢失未保存的更改。
如果文档自打开或保存文件以来已被修改,我们称之为dirty
文档。让我们在Document
中添加一个dirty
变量并将其初始化为false
。我们不希望这个变量从外部被修改,因此我们添加了一个只读的is_dirty
函数到Document
。我们还会在任何文本更改时将其设置为true
,并在保存时设置为false
。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| @@ -7,6 +7,7 @@ use std::io::{Error, Write}; pub struct Document { rows: Vec<Row>, pub file_name: Option<String>, + dirty: bool, } impl Document { @@ -19,6 +20,7 @@ impl Document { Ok(Self { rows, file_name: Some(filename.to_string()), + dirty: false, }) } pub fn row(&self, index: usize) -> Option<&Row> { @@ -32,9 +34,6 @@ impl Document { } fn insert_new_line(&mut self, at: &Position) { - if at.y > self.len() { - return; - } if at.y == self.len() { self.rows.push(Row::default()); return; @@ -45,6 +44,10 @@ impl Document { } pub fn insert(&mut self, at: &Position, c: char) { + if at.y > self.len() { + return; + } + self.dirty = true; if c == '\n' { self.insert_new_line(at); return; @@ -53,7 +56,7 @@ impl Document { let mut row = Row::default(); row.insert(0, c); self.rows.push(row); - } else if at.y < self.len() { + } else { let row = self.rows.get_mut(at.y).unwrap(); row.insert(at.x, c); } @@ -64,6 +67,7 @@ impl Document { if at.y >= len { return; } + self.dirty = true; if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 { let next_row = self.rows.remove(at.y + 1); let row = self.rows.get_mut(at.y).unwrap(); @@ -74,14 +78,19 @@ impl Document { } } - pub fn save(&self) -> Result<(), Error> { + pub fn save(&mut self) -> Result<(), Error> { if let Some(file_name) = &self.file_name { let mut file = fs::File::create(file_name)?; for row in &self.rows { file.write_all(row.as_bytes())?; file.write_all(b"\n")?; } + self.dirty = false; } Ok(()) } + + pub fn is_dirty(&self) -> bool { + self.dirty + } }
|
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @@ -264,12 +264,22 @@ impl Editor { fn draw_status_bar(&self) { let mut status; let width = self.terminal.size().width as usize; + let modified_indicator = if self.document.is_dirty() { + "(modified)" + } else { + "" + }; let mut file_name = "[No Name]".to_string(); if let Some(name) = &self.document.file_name { file_name = name.clone(); file_name.truncate(20); } - status = format!("{} - {} lines", file_name, self.document.len()); + status = format!( + "{} - {} lines {}", + file_name, + self.document.len(), + modified_indicator + ); let line_indicator = format!( "{}/{}", self.cursor_position.y.saturating_add(1),
|
我们还重新设置了insert
中的边界处理。它现在和delete
中的检查类似。
确认退出
如果用户尝试退出时,向他们警告未保存的更改。如果document.is_dirty()
为true
,我们将在状态栏中显示一个警告,并要求用户再按三次Ctrl-Q
才能不保存而退出。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @@ -10,6 +11,7 @@ use termion::event::Key; const VERSION: &str = env!("CARGO_PKG_VERSION"); const STATUS_BG_COLOR: color::LightWhite = color::LightWhite; const STATUS_FG_COLOR: color::Black = color::Black; +const QUIT_TIMES: u8 = 3; #[derive(Default)] pub struct Position { @@ -37,6 +39,7 @@ pub struct Editor { document: Document, offset: Position, status_message: StatusMessage, + quit_times: u8, } impl Editor { @@ -77,6 +80,7 @@ impl Editor { document, offset: Position::default(), status_message: StatusMessage::form(initial_status), + quit_times: QUIT_TIMES, } } @@ -120,7 +124,17 @@ impl Editor { fn process_keypress(&mut self) -> Result<(), std::io::Error> { let pressed_key = Terminal::read_key()?; match pressed_key { - Key::Ctrl('q') => self.should_quit = true, + Key::Ctrl('q') => { + if self.quit_times > 0 && self.document.is_dirty() { + self.status_message = StatusMessage::form(format!( + "WARNING! File has unsaved changes. Press Ctrl-Q {} more times to quit.", + self.quit_times + )); + self.quit_times -= 1; + return Ok(()); + } + self.should_quit = true; + } Key::Ctrl('s') => self.save(), Key::Char(c) => { self.document.insert(&self.cursor_position, c); @@ -144,6 +158,10 @@ impl Editor { _ => (), } self.scroll(); + if self.quit_times < QUIT_TIMES { + self.quit_times = QUIT_TIMES; + self.status_message = StatusMessage::form(String::new()); + } Ok(()) }
|
我们增加了一个新的常量,用于计算我们需要用户按下Ctrl-Q
的附加次数,并将其作为Editor
中的一个额外字段。当文档有未保存的更改并且用户尝试退出时,我们将quit_times
计数减少,直到其达到0
,然后最终退出。请注意,我们是在match
对退出的分支内返回。这样,只有在用户按下的不是Ctrl-Q
的其他键后,match
之后的代码才会被调用,因此我们可以在match
之后检查quit_times
是否被修改,并在必要时重置它。
代码审查
现在,文本编辑器iTEditor
的基本功能就已经全部实现了。现在我们可以回过头来检查一下我们的代码是否没有问题了,是不是存在一些潜在的问题。比如之前,我们为了防止溢出使用了saturated_adds
。
我们给Clippy引入一些新的技巧。
src/main.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @@ -1,4 +1,13 @@ -#![allow(non_snake_case)] +#![warn(clippy::all, clippy::pedantic, clippy::restriction)] +#![allow( + non_snake_case, + clippy::missing_docs_in_private_items, + clippy::implicit_return, + clippy::shadow_reuse, + clippy::print_stdout, + clippy::wildcard_enum_match_arm, + clippy::else_if_without_else +)] mod document; mod editor;
|
clippy::restriction
包含了许多可能会或不会导致代码中错误的警告。现在运行cargo clippy
时,可以看到非常多的问题!幸运的是,每个条目都附带了一个链接,链接中包含了一些解释。我们还为iTEditor
禁用了一些条目,这样Clippy
的结果更加易于管理。
现在我们按照Clippy
的指导开始修改。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @@ -34,11 +34,15 @@ impl Document { } fn insert_new_line(&mut self, at: &Position) { + if at.y > self.len() { + return; + } if at.y == self.len() { self.rows.push(Row::default()); return; } else { let new_row = self.rows.get_mut(at.y).unwrap().split(at.x); + #[allow(clippy::integer_arithmetic)] self.rows.insert(at.y + 1, new_row); } } @@ -62,13 +66,14 @@ impl Document { } } + #[allow(clippy::integer_arithmetic)] pub fn delete(&mut self, at: &Position) { let len = self.len(); if at.y >= len { return; } self.dirty = true; - if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 { + if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y + 1 < len { let next_row = self.rows.remove(at.y + 1); let row = self.rows.get_mut(at.y).unwrap(); row.append(&next_row);
|
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @@ -204,14 +204,14 @@ impl Editor { Key::End => x = width, Key::PageUp => { y = if y > terminal_height { - y - terminal_height + y.saturating_div(terminal_height) } else { 0 } } Key::PageDown => { y = if y.saturating_add(terminal_height) < height { - y + terminal_height as usize + y.saturating_add(terminal_height) } else { height } @@ -233,7 +233,10 @@ impl Editor { let height = self.terminal.size().height; for terminal_row in 0..height { Terminal::clear_current_line(); - if let Some(row) = self.document.row(terminal_row as usize + self.offset.y) { + if let Some(row) = self + .document + .row(self.offset.y.saturating_add(terminal_row as usize)) + { self.draw_row(row); } else if self.document.is_empty() && terminal_row == height / 3 { self.draw_welcome_message(); @@ -257,7 +260,7 @@ impl Editor { pub fn draw_row(&self, row: &Row) { let start = self.offset.x; let width = self.terminal.size().width as usize; - let end = self.offset.x + width; + let end = self.offset.x.saturating_add(width); let row = row.render(start, end); println!("{}\r", row) } @@ -302,10 +305,9 @@ impl Editor { self.cursor_position.y.saturating_add(1), self.document.len() ); + #[allow(clippy::integer_arithmetic)] let len = status.len() + line_indicator.len(); - if width > len { - status.push_str(&" ".repeat(width - len)); - } + status.push_str(&" ".repeat(width.saturating_sub(len))); status = format!("{}{}", status, line_indicator); status.truncate(width); Terminal::set_bg_color(STATUS_BG_COLOR); @@ -330,11 +332,7 @@ impl Editor { self.status_message = StatusMessage::form(format!("{}{}", prompt, result)); self.refresh_screen()?; match Terminal::read_key()? { - Key::Backspace => { - if !result.is_empty() { - result.truncate(result.len() - 1); - } - } + Key::Backspace => result.truncate(result.len().saturating_sub(1)), Key::Char('\n') => break, Key::Char(c) => { if !c.is_control() {
|
src/row.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @@ -23,6 +23,7 @@ impl Row { let end = cmp::min(end, self.string.len()); let start = cmp::min(start, end); let mut result = String::new(); + #[allow(clippy::integer_arithmetic)] for grapheme in self.string[..] .graphemes(true) .skip(start) @@ -59,6 +60,7 @@ impl Row { self.update_len(); } + #[allow(clippy::integer_arithmetic)] pub fn delete(&mut self, at: usize) { if at >= self.len() { return;
|
主要修改了一下几个部分:
- 在我们上一次修改中,
insert_newline
没有进行边界检查。仅从insert_newline
来看,因为现在insert_newline
仅被insert
调用,在那里我们已经进行了边界检查。这意味着存在一个对调用insert_newline
的隐含规则,确保at.y
不会超过当前文档的长度。我们现在已经纠正了这一点。
- 我们用加法替换了
len
的减法,变成了对at.y
的加法。因为在那个函数中我们可以很容易看到y
总是小于len
,所以总有空间进行+1
。不是很明显的是,len
总是会大于0
。
- 在使用
saturating_sub
时,我们能够去除一些大小比较,可以简化一些代码。
Clippy
还给了我们一些关于整数除法的警告。问题是这样的:例如,如果你做除法,100/3
,结果将是33
,余数会被移除。在我们的程序中,这是可以的,但是解决这个问题的原因是相同的:任何审核我们代码的人都不能确定我们是否考虑到了这一点,或者简单地忘记了。我们能做的最少的事情是留下一个注释或一个clippy
指令。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @@ -229,6 +229,7 @@ impl Editor { self.cursor_position = Position { x, y } } + #[allow(clippy::integer_division, clippy::integer_arithmetic)] fn draw_rows(&self) { let height = self.terminal.size().height; for terminal_row in 0..height { @@ -250,6 +251,7 @@ impl Editor { let mut welcome_message = format!("iTEditor -- version {}\r", VERSION); let width = self.terminal.size().width as usize; let len = welcome_message.len(); + #[allow(clippy::integer_arithmetic, clippy::integer_division)] let padding = width.saturating_sub(len) / 2; let spaces = " ".repeat(padding.saturating_sub(1)); welcome_message = format!("~{}{}", spaces, welcome_message);
|
现在,让我们来解决 clippy 的下一个问题。
src/document.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @@ -34,21 +34,22 @@ impl Document { } fn insert_new_line(&mut self, at: &Position) { - if at.y > self.len() { + if at.y > self.rows.len() { return; } - if at.y == self.len() { + if at.y == self.rows.len() { self.rows.push(Row::default()); return; } else { - let new_row = self.rows.get_mut(at.y).unwrap().split(at.x); + #[allow(clippy::indexing_slicing)] + let new_row = self.rows[at.y].split(at.x); #[allow(clippy::integer_arithmetic)] self.rows.insert(at.y + 1, new_row); } } pub fn insert(&mut self, at: &Position, c: char) { - if at.y > self.len() { + if at.y > self.rows.len() { return; } self.dirty = true; @@ -56,29 +57,30 @@ impl Document { self.insert_new_line(at); return; } - if at.y == self.len() { + if at.y == self.rows.len() { let mut row = Row::default(); row.insert(0, c); self.rows.push(row); } else { - let row = self.rows.get_mut(at.y).unwrap(); + #[allow(clippy::indexing_slicing)] + let row = &mut self.rows[at.y]; row.insert(at.x, c); } } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::integer_arithmetic, clippy::indexing_slicing)] pub fn delete(&mut self, at: &Position) { - let len = self.len(); + let len = self.rows.len(); if at.y >= len { return; } self.dirty = true; - if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y + 1 < len { + if at.x == self.rows[at.y].len() && at.y + 1 < len { let next_row = self.rows.remove(at.y + 1); - let row = self.rows.get_mut(at.y).unwrap(); + let row = &mut self.rows[at.y]; row.append(&next_row); } else { - let row = self.rows.get_mut(at.y).unwrap(); + let row = &mut self.rows[at.y]; row.delete(at.x); } }
|
这个变更主要与在某些位置访问行有关。我们之前使用的是安全的get_mut
方法,即便没有内容可检索也不会引发panic
,比如因为索引错误。我们直接在其上调用了unwrap()
,这实际上否定了首先使用get_mut
的好处。我们现在已经替换了这种做法,改为直接访问self.rows
。我们在每个这样做的地方留下了一个clippy
声明,以表明我们确实在那个时刻检查了我们仅仅在访问有效的索引。
之前还有一个隐含的约定,那就是文档的长度始终对应于其中的行数,因此我们到处都在调用self.len()
而不是self.rows.len()
。如果我们的文档更长,所有在self.row
上的操作都会失效。
好了,现在还有两个clippy
警告要解决。
src/editor.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @@ -59,11 +59,10 @@ impl Editor { pub fn default() -> Self { let args: Vec<String> = env::args().collect(); let mut initial_status = String::from("HELP: Ctrl-S = save | Ctrl-Q = quit"); - let document = if args.len() > 1 { - let file_name = &args[1]; - let doc = Document::open(&file_name); - if doc.is_ok() { - doc.unwrap() + let document = if let Some(file_name) = args.get(1) { + let doc = Document::open(file_name); + if let Ok(doc) = doc { + doc } else { initial_status = format!("ERRROR: Could not open file {}", file_name); Document::default()
|
get
函数有一个特点,它仅在值存在时才返回值给我们,这消除了索引前进行检查的需要。然后,我们去除了is_ok
以支持if let
结构,以此来省去unwrap
。
现在,还有一些问题是Clippy
检测不到的,我们不应该仅仅依赖Clippy
进行开发。接下来我们将处理这些问题。
性能改进
目前我们的编辑器并没有做太多事情。但是,我们可以做一些性能改进。性能调整是一个复杂的话题,因为我们需要在可读、可维护的代码和超级优化、难以阅读和维护的代码之间找到平衡。我们并不期望iTEditor
成为最快的编辑器,但关注一些性能方面的事情是有意义的。
我希望我们能注意到在遍历文档时对行进行不必要的迭代,以及在从左到右遍历行时对字符进行不必要的迭代。现在我们就专注于这些问题,不做额外的缓存,也不用花哨的技巧,只是寻找重复的操作。
让我们关注一下我们如何处理行。我们有一个我们多次重复的通用模式。例如,这里的insert
:
1 2 3 4 5 6 7 8 9 10 11 12
| pub fn insert(&mut self, at: usize, c: char) { if at >= self.len() { self.string.push(c); } else { let mut resule: String = self.string[..].graphemes(true).take(at).collect(); let remainder: String = self.string[..].graphemes(true).skip(at).collect(); resule.push(c); resule.push_str(&remainder); self.string = resule; } self.update_len(); }
|
在这个实现中,我们对字符串进行了三次迭代:
- 从开始到
at
位置计算result
。
- 第二次从开始到结束(忽略开始和
at
之间的所有内容)来计算remainder
。
- 然后整个字符串迭代一次来更新
len
。
这并不理想。让我们来优化一下。
src/row.rs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| @@ -9,12 +9,10 @@ pub struct Row { impl From<&str> for Row { fn from(slice: &str) -> Self { - let mut row = Self { + Self { string: String::from(slice), - len: 0, - }; - row.update_len(); - row + len: slice.graphemes(true).count(), + } } } @@ -43,47 +41,69 @@ impl Row { pub fn is_empty(&self) -> bool { self.len == 0 } - pub fn update_len(&mut self) { - self.len = self.string[..].graphemes(true).count(); - } pub fn insert(&mut self, at: usize, c: char) { if at >= self.len() { self.string.push(c); - } else { - let mut resule: String = self.string[..].graphemes(true).take(at).collect(); - let remainder: String = self.string[..].graphemes(true).skip(at).collect(); - resule.push(c); - resule.push_str(&remainder); - self.string = resule; + self.len += 1; + return; } - self.update_len(); + let mut result: String = String::new(); + let mut length = 0; + for (index, grapheme) in self.string[..].graphemes(true).enumerate() { + length += 1; + if index == at { + length += 1; + result.push(c); + } + result.push_str(grapheme); + } + self.len = length; + self.string = result; } - #[allow(clippy::integer_arithmetic)] pub fn delete(&mut self, at: usize) { if at >= self.len() { return; - } else { - let mut result: String = self.string[..].graphemes(true).take(at).collect(); - let remainder: String = self.string[..].graphemes(true).skip(at + 1).collect(); - result.push_str(&remainder); - self.string = result; } - self.update_len(); + let mut result: String = String::new(); + let mut length = 0; + for (index, grapheme) in self.string[..].graphemes(true).enumerate() { + if index != at { + length += 1; + result.push_str(grapheme); + } + } + self.len = length; + self.string = result; } pub fn append(&mut self, new_row: &Self) { self.string = format!("{}{}", self.string, new_row.string); - self.update_len(); + self.len += new_row.len; } pub fn split(&mut self, at: usize) -> Self { - let beginning: String = self.string[..].graphemes(true).take(at).collect(); - let remainder: String = self.string[..].graphemes(true).skip(at).collect(); - self.string = beginning; - self.update_len(); - Self::from(&remainder[..]) + let mut row: String = String::new(); + let mut length = 0; + let mut splitted_row: String = String::new(); + let mut splitted_length = 0; + for (index, grapheme) in self.string[..].graphemes(true).enumerate() { + if index < at { + length += 1; + row.push_str(grapheme); + } else { + splitted_length += 1; + splitted_row.push_str(grapheme); + } + } + self.string = row; + self.len = length; + + Self { + string: splitted_row, + len: splitted_length, + } } pub fn as_bytes(&self) -> &[u8] {
|
我们在这里做了两件事:
- 我们放弃了
update_len
,因为我们现在在每次行操作时都会手动计算长度。
- 我们正在对
enumerate
进行迭代,这不仅提供了下一个元素,还提供了迭代器中的索引。这样,我们就可以在遍历行时轻松计算长度。
本文链接: https://zone.ivanz.cc/p/a052054.html