Previous chapter - Overview - Appendices - Next Chapter
Now that hecto
can read files, let’s see if we can teach it to edit files as
well.
Insert ordinary characters
Let’s begin by writing a function that inserts a single character into a
Document
, at a given position. We start by allowing to add a character into
our string at a given position.
@@ -1,3 +1,4 @@
|
|
1
|
+
use crate::Position;
|
1
2
|
use crate::Row;
|
2
3
|
use std::fs;
|
3
4
|
|
@@ -28,4 +29,14 @@ impl Document {
|
|
28
29
|
pub fn len(&self) -> usize {
|
29
30
|
self.rows.len()
|
30
31
|
}
|
32
|
+
pub fn insert(&mut self, at: &Position, c: char) {
|
33
|
+
if at.y == self.len() {
|
34
|
+
let mut row = Row::default();
|
35
|
+
row.insert(0, c);
|
36
|
+
self.rows.push(row);
|
37
|
+
} else if at.y < self.len() {
|
38
|
+
let row = self.rows.get_mut(at.y).unwrap();
|
39
|
+
row.insert(at.x, c);
|
40
|
+
}
|
41
|
+
}
|
31
42
|
}
|
@@ -101,6 +101,7 @@ impl Editor {
|
|
101
101
|
let pressed_key = Terminal::read_key()?;
|
102
102
|
match pressed_key {
|
103
103
|
Key::Ctrl('q') => self.should_quit = true,
|
104
|
+
Key::Char(c) => self.document.insert(&self.cursor_position, c),
|
104
105
|
Key::Up
|
105
106
|
| Key::Down
|
106
107
|
| Key::Left
|
@@ -1,6 +1,7 @@
|
|
1
1
|
use std::cmp;
|
2
2
|
use unicode_segmentation::UnicodeSegmentation;
|
3
3
|
|
4
|
+
#[derive(Default)]
|
4
5
|
pub struct Row {
|
5
6
|
string: String,
|
6
7
|
len: usize,
|
@@ -44,4 +45,16 @@ impl Row {
|
|
44
45
|
fn update_len(&mut self) {
|
45
46
|
self.len = self.string[..].graphemes(true).count();
|
46
47
|
}
|
48
|
+
pub fn insert(&mut self, at: usize, c: char) {
|
49
|
+
if at >= self.len() {
|
50
|
+
self.string.push(c);
|
51
|
+
} else {
|
52
|
+
let mut result: String = self.string[..].graphemes(true).take(at).collect();
|
53
|
+
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
|
54
|
+
result.push(c);
|
55
|
+
result.push_str(&remainder);
|
56
|
+
self.string = result;
|
57
|
+
}
|
58
|
+
self.update_len();
|
59
|
+
}
|
47
60
|
}
|
Let’s focus first on the changes to Row
.
Here, we handle two cases: If we happen to be at the end of our string, we push the character onto it. This can happen if the user is at the end of a line and keeps typing. If not, we are rebuilding our string by going through it character by character.
We use the iterator’s take
and skip
functions to create new iterators, one
that goes from 0
to at
(including at
), and another one that goes from the
element after at
to the end. We use collect
to combine these iterators into
strings. collect
is very powerful and can collect into different
collections.
Since there are multiple collections collect
can create, we have to provide
the type of result
and remainder
, otherwise Rust wouldn’t know what kind of
collection to create.
We’re now also deriving default
for Row
. We’ll use that in Document
.
Similar to what we did in Row
, we are handling the case where the user is
attempting to insert at the bottom of our document, for which case we create a
new row.
Now we need to call that method when a character is entered. We do that by
extending process_keypress
in editor
.
With that change, we can now add characters anywhere in the document. But our cursor does not move - so we are essentially typing in our text backwards. Let’s fix that now by treating “Enter a character” as “Enter a character and go to the right”.
@@ -101,7 +101,10 @@ impl Editor {
|
|
101
101
|
let pressed_key = Terminal::read_key()?;
|
102
102
|
match pressed_key {
|
103
103
|
Key::Ctrl('q') => self.should_quit = true,
|
104
|
-
Key::Char(c) =>
|
104
|
+
Key::Char(c) => {
|
105
|
+
self.document.insert(&self.cursor_position, c);
|
106
|
+
self.move_cursor(Key::Right);
|
107
|
+
}
|
105
108
|
Key::Up
|
106
109
|
| Key::Down
|
107
110
|
| Key::Left
|
You should now be able to confirm that putting in characters works, even at the bottom of the file.
Simple deletion
We now want Backspace and Delete to work.
Let’s start with Delete, which should remove the character under the cursor. If
your cursor is a line, |
, instead of a block, “under the cursor” means “in
front of the cursor”, since the cursor is a blinking line displayed on the left
side of its position. Let’s start by adding a delete
function on a row
.
@@ -39,4 +39,11 @@ impl Document {
|
|
39
39
|
row.insert(at.x, c);
|
40
40
|
}
|
41
41
|
}
|
42
|
+
pub fn delete(&mut self, at: &Position) {
|
43
|
+
if at.y >= self.len() {
|
44
|
+
return;
|
45
|
+
}
|
46
|
+
let row = self.rows.get_mut(at.y).unwrap();
|
47
|
+
row.delete(at.x);
|
48
|
+
}
|
42
49
|
}
|
@@ -105,6 +105,7 @@ impl Editor {
|
|
105
105
|
self.document.insert(&self.cursor_position, c);
|
106
106
|
self.move_cursor(Key::Right);
|
107
107
|
}
|
108
|
+
Key::Delete => self.document.delete(&self.cursor_position),
|
108
109
|
Key::Up
|
109
110
|
| Key::Down
|
110
111
|
| Key::Left
|
@@ -57,4 +57,15 @@ impl Row {
|
|
57
57
|
}
|
58
58
|
self.update_len();
|
59
59
|
}
|
60
|
+
pub fn delete(&mut self, at: usize) {
|
61
|
+
if at >= self.len() {
|
62
|
+
return;
|
63
|
+
} else {
|
64
|
+
let mut result: String = self.string[..].graphemes(true).take(at).collect();
|
65
|
+
let remainder: String = self.string[..].graphemes(true).skip(at + 1).collect();
|
66
|
+
result.push_str(&remainder);
|
67
|
+
self.string = result;
|
68
|
+
}
|
69
|
+
self.update_len();
|
70
|
+
}
|
60
71
|
}
|
As you can see, the code is very similar to the insert
code we wrote before.
The difference is that in Row
, we are not adding a character, but we are
skipping the one we want to delete when glueing together result
and
remainder
. And in Document
, we do not need to handle the case where we want
to delete a row (yet), which makes that code a bit simpler than the symmertrical
insert
code.
You should now be able to delete characters from with in a line. Let’s tackle
Backspace next: Essentially, Backspace is a combination of going left and
deleting, so we adjust process_keypress
as follows:
@@ -106,6 +106,12 @@ impl Editor {
|
|
106
106
|
self.move_cursor(Key::Right);
|
107
107
|
}
|
108
108
|
Key::Delete => self.document.delete(&self.cursor_position),
|
109
|
+
Key::Backspace => {
|
110
|
+
if self.cursor_position.x > 0 || self.cursor_position.y > 0 {
|
111
|
+
self.move_cursor(Key::Left);
|
112
|
+
self.document.delete(&self.cursor_position);
|
113
|
+
}
|
114
|
+
}
|
109
115
|
Key::Up
|
110
116
|
| Key::Down
|
111
117
|
| Key::Left
|
Backspace now works within a line. We even make sure that if we are at the beginning of the document, we are not doing a delete - otherwise, we would start removing elements behind the cursor.
If you do a backspace at the beginning of a line, however, it moves up a line without doing anything else. Let’s fix that in the next sections.
Complex deletion
There are two edge cases which we can’t handle right now, and that is either using Backspace at the beginning of a line, or using Delete at the end of a line. In our case, Backspace simply goes to the left, which, at the beginning of the line, means to go to the end of the previous line, and then attempts to delete a character. This means that the Backspace case will be solved as soon as we allow a delete at the end of a line.
@@ -40,10 +40,17 @@ impl Document {
|
|
40
40
|
}
|
41
41
|
}
|
42
42
|
pub fn delete(&mut self, at: &Position) {
|
43
|
-
|
43
|
+
let len = self.len();
|
44
|
+
if at.y >= len {
|
44
45
|
return;
|
45
46
|
}
|
46
|
-
|
47
|
-
|
47
|
+
if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 {
|
48
|
+
let next_row = self.rows.remove(at.y + 1);
|
49
|
+
let row = self.rows.get_mut(at.y).unwrap();
|
50
|
+
row.append(&next_row);
|
51
|
+
} else {
|
52
|
+
let row = self.rows.get_mut(at.y).unwrap();
|
53
|
+
row.delete(at.x);
|
54
|
+
}
|
48
55
|
}
|
49
56
|
}
|
@@ -68,4 +68,8 @@ impl Row {
|
|
68
68
|
}
|
69
69
|
self.update_len();
|
70
70
|
}
|
71
|
+
pub fn append(&mut self, new: &Self) {
|
72
|
+
self.string = format!("{}{}", self.string, new.string);
|
73
|
+
self.update_len();
|
74
|
+
}
|
71
75
|
}
|
We start by giving Row
the ability to append another row to it. We use this
ability in Document
. Now, the code in Document
looks a bit complex, and I’m
going to explain in a second why this is the case. What it does, though, is
checking if we are at the end of a line, and if a line follows after this one.
If that’s the case, we remove the next line from our vec
and append it to the
current row. If that’s not the case, we simply try to delete from the current
row.
Now, why does the code look so complicated? Can’t we just move the definition of
row
up above the if
statement to make things clearer?
This is our second big encounter with Rust’s borrow checker. We can’t have two mutable references to elements within the vector at the same time, and we can’t mutate the vector while we have a mutable reference to an element in it. Why? Because let’s say we have a vector with A, B and C in it, and we have a reference to B. A reference is like a pointer which points to where B resides in the memory. Now we remove A, which causes B and C to move to the left. Our reference would suddenly no longer point to B, but to C!
This means that we can’t have a reference to row
and then delete part of the
vector. So we first read row
’s length directly without retaining a reference.
Then we mutate the vector by removing an element from it, and then we create our
mutable reference to row
.
Try rewriting the code if you want to, and check what the compiler tells you in case you are interested.
The Enter key
The last editor operation we have to implement is the Enter key. The Enter key allows the user to insert new lines into the text, or split a line into two lines. You can actually add newlines that way right now, but as you might expect, the handling is less than optimal. This is because the newlines are inserted as part of the row instead of resulting in the creation of a new row.
Let’s start with the easy case, adding a new row below the current one.
@@ -29,7 +29,22 @@ impl Document {
|
|
29
29
|
pub fn len(&self) -> usize {
|
30
30
|
self.rows.len()
|
31
31
|
}
|
32
|
+
fn insert_newline(&mut self, at: &Position) {
|
33
|
+
if at.y > self.len() {
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
let new_row = Row::default();
|
37
|
+
if at.y == self.len() || at.y.saturating_add(1) == self.len() {
|
38
|
+
self.rows.push(new_row);
|
39
|
+
} else {
|
40
|
+
self.rows.insert(at.y + 1, new_row)
|
41
|
+
}
|
42
|
+
}
|
32
43
|
pub fn insert(&mut self, at: &Position, c: char) {
|
44
|
+
if c == '\n' {
|
45
|
+
self.insert_newline(at);
|
46
|
+
return;
|
47
|
+
}
|
33
48
|
if at.y == self.len() {
|
34
49
|
let mut row = Row::default();
|
35
50
|
row.insert(0, c);
|
We call insert_newline
from insert
in case a newline is handed to us. In
insert_newline
, we are checking if enter was pressed either on the last row of
the document, or one row below it (remember that we allow navigating there). If
that is the case, we push a new row at the end of our vec
. If that’s not the
case, we insert a new row at the correct position.
Let’s now handle the case where we are in the middle of a row.
@@ -33,12 +33,12 @@ impl Document {
|
|
33
33
|
if at.y > self.len() {
|
34
34
|
return;
|
35
35
|
}
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
if at.y == self.len() {
|
37
|
+
self.rows.push(Row::default());
|
38
|
+
return;
|
39
|
-
} else {
|
40
|
-
self.rows.insert(at.y + 1, new_row)
|
41
39
|
}
|
40
|
+
let new_row = self.rows.get_mut(at.y).unwrap().split(at.x);
|
41
|
+
self.rows.insert(at.y + 1, new_row);
|
42
42
|
}
|
43
43
|
pub fn insert(&mut self, at: &Position, c: char) {
|
44
44
|
if c == '\n' {
|
@@ -72,4 +72,11 @@ impl Row {
|
|
72
72
|
self.string = format!("{}{}", self.string, new.string);
|
73
73
|
self.update_len();
|
74
74
|
}
|
75
|
+
pub fn split(&mut self, at: usize) -> Self {
|
76
|
+
let beginning: String = self.string[..].graphemes(true).take(at).collect();
|
77
|
+
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
|
78
|
+
self.string = beginning;
|
79
|
+
self.update_len();
|
80
|
+
Self::from(&remainder[..])
|
81
|
+
}
|
75
82
|
}
|
We have now added a method called split
, which truncates the current row up
until a given index, and returns another row with everything behind that index.
On Document, we are now only pushing an empty row if we are below the last line,
in any other case, we are changing the current row with split
and insert the
result. This works even at the end of a line, in which case the new row would
simply contain an empty string.
Great! Now we can move around our document, add whitespaces, characters, even emojis, remove lines and so on. But editing is obviously useless without saving, so let’s handle that next.
Save to disk
Now that we’ve finally made text editable, let’s implement saving to disk. We
start with implementing a save
method in Document
:
@@ -1,6 +1,7 @@
|
|
1
1
|
use crate::Position;
|
2
2
|
use crate::Row;
|
3
3
|
use std::fs;
|
4
|
+
use std::io::{Error, Write};
|
4
5
|
|
5
6
|
#[derive(Default)]
|
6
7
|
pub struct Document {
|
@@ -68,4 +69,14 @@ impl Document {
|
|
68
69
|
row.delete(at.x);
|
69
70
|
}
|
70
71
|
}
|
72
|
+
pub fn save(&self) -> Result<(), Error> {
|
73
|
+
if let Some(file_name) = &self.file_name {
|
74
|
+
let mut file = fs::File::create(file_name)?;
|
75
|
+
for row in &self.rows {
|
76
|
+
file.write_all(row.as_bytes())?;
|
77
|
+
file.write_all(b"\n")?;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
Ok(())
|
81
|
+
}
|
71
82
|
}
|
@@ -55,7 +55,7 @@ impl Editor {
|
|
55
55
|
}
|
56
56
|
pub fn default() -> Self {
|
57
57
|
let args: Vec<String> = env::args().collect();
|
58
|
-
let mut initial_status = String::from("HELP: Ctrl-Q = quit");
|
58
|
+
let mut initial_status = String::from("HELP: Ctrl-S = save | Ctrl-Q = quit");
|
59
59
|
let document = if args.len() > 1 {
|
60
60
|
let file_name = &args[1];
|
61
61
|
let doc = Document::open(&file_name);
|
@@ -101,6 +101,14 @@ impl Editor {
|
|
101
101
|
let pressed_key = Terminal::read_key()?;
|
102
102
|
match pressed_key {
|
103
103
|
Key::Ctrl('q') => self.should_quit = true,
|
104
|
+
Key::Ctrl('s') => {
|
105
|
+
if self.document.save().is_ok() {
|
106
|
+
self.status_message =
|
107
|
+
StatusMessage::from("File saved successfully.".to_string());
|
108
|
+
} else {
|
109
|
+
self.status_message = StatusMessage::from("Error writing file!".to_string());
|
110
|
+
}
|
111
|
+
}
|
104
112
|
Key::Char(c) => {
|
105
113
|
self.document.insert(&self.cursor_position, c);
|
106
114
|
self.move_cursor(Key::Right);
|
@@ -79,4 +79,7 @@ impl Row {
|
|
79
79
|
self.update_len();
|
80
80
|
Self::from(&remainder[..])
|
81
81
|
}
|
82
|
+
pub fn as_bytes(&self) -> &[u8] {
|
83
|
+
self.string.as_bytes()
|
84
|
+
}
|
82
85
|
}
|
We extend Row
with a method that allows us to convert the row into a byte
array. In Document
, write_all
takes that byte array and writes it to disk.
Since our rows do not contain the newline symbol, we write it out separately.
The b
in front of the newline string indicates that this is a byte array and
not a string.
Since writing may cause errors, our save
function returns a Result
, and we
are using the ?
again to pass any errors that might occur to the caller.
In Document
, we connect save
to Ctrl-S. We check if the write was
successful by using is_ok
, which returns true
in case a Result
is Ok
and
not an Err
, and we set the status message accordingly.
Last but not least, we change the initial status to tell our user how to write a file.
Great, now we can open, adjust and save our files!
Save as…
Currently, when the user runs hecto
with no arguments, they get a blank file
to edit but have no way of saving. Let’s make a prompt()
function that
displays a prompt in the status bar, and lets the user input a line of text
after the prompt:
@@ -102,6 +102,9 @@ impl Editor {
|
|
102
102
|
match pressed_key {
|
103
103
|
Key::Ctrl('q') => self.should_quit = true,
|
104
104
|
Key::Ctrl('s') => {
|
105
|
+
if self.document.file_name.is_none() {
|
106
|
+
self.document.file_name = Some(self.prompt("Save as: ")?);
|
107
|
+
}
|
105
108
|
if self.document.save().is_ok() {
|
106
109
|
self.status_message =
|
107
110
|
StatusMessage::from("File saved successfully.".to_string());
|
@@ -280,6 +283,23 @@ impl Editor {
|
|
280
283
|
print!("{}", text);
|
281
284
|
}
|
282
285
|
}
|
286
|
+
fn prompt(&mut self, prompt: &str) -> Result<String, std::io::Error> {
|
287
|
+
let mut result = String::new();
|
288
|
+
loop {
|
289
|
+
self.status_message = StatusMessage::from(format!("{}{}", prompt, result));
|
290
|
+
self.refresh_screen()?;
|
291
|
+
if let Key::Char(c) = Terminal::read_key()? {
|
292
|
+
if c == '\n' {
|
293
|
+
self.status_message = StatusMessage::from(String::new());
|
294
|
+
break;
|
295
|
+
}
|
296
|
+
if !c.is_control() {
|
297
|
+
result.push(c);
|
298
|
+
}
|
299
|
+
}
|
300
|
+
}
|
301
|
+
Ok(result)
|
302
|
+
}
|
283
303
|
}
|
284
304
|
|
285
305
|
fn die(e: std::io::Error) {
|
The user’s input is stored in result
, which we initialize as an empty string.
We enter an infinite loop that repeatadly sets the status message, refreshes the
screen, and waits for a key press to handle. When the user presses enter, the
status message is cleared and the message is returned. The errors which might
occur on the way are propagated up.
Now that the user can save the file, let’s handle a few more cases in our prompt. Let’s now allow the user to cancel and backspace, and let’s also treat an empty input as cancelling.
@@ -97,21 +97,27 @@ impl Editor {
|
|
97
97
|
Terminal::cursor_show();
|
98
98
|
Terminal::flush()
|
99
99
|
}
|
100
|
+
fn save(&mut self) {
|
101
|
+
if self.document.file_name.is_none() {
|
102
|
+
let new_name = self.prompt("Save as: ").unwrap_or(None);
|
103
|
+
if new_name.is_none() {
|
104
|
+
self.status_message = StatusMessage::from("Save aborted.".to_string());
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
self.document.file_name = new_name;
|
108
|
+
}
|
109
|
+
|
110
|
+
if self.document.save().is_ok() {
|
111
|
+
self.status_message = StatusMessage::from("File saved successfully.".to_string());
|
112
|
+
} else {
|
113
|
+
self.status_message = StatusMessage::from("Error writing file!".to_string());
|
114
|
+
}
|
115
|
+
}
|
100
116
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
101
117
|
let pressed_key = Terminal::read_key()?;
|
102
118
|
match pressed_key {
|
103
119
|
Key::Ctrl('q') => self.should_quit = true,
|
104
|
-
Key::Ctrl('s') =>
|
120
|
+
Key::Ctrl('s') => self.save(),
|
105
|
-
if self.document.file_name.is_none() {
|
106
|
-
self.document.file_name = Some(self.prompt("Save as: ")?);
|
107
|
-
}
|
108
|
-
if self.document.save().is_ok() {
|
109
|
-
self.status_message =
|
110
|
-
StatusMessage::from("File saved successfully.".to_string());
|
111
|
-
} else {
|
112
|
-
self.status_message = StatusMessage::from("Error writing file!".to_string());
|
113
|
-
}
|
114
|
-
}
|
115
121
|
Key::Char(c) => {
|
116
122
|
self.document.insert(&self.cursor_position, c);
|
117
123
|
self.move_cursor(Key::Right);
|
@@ -283,22 +289,35 @@ impl Editor {
|
|
283
289
|
print!("{}", text);
|
284
290
|
}
|
285
291
|
}
|
286
|
-
fn prompt(&mut self, prompt: &str) -> Result<String
|
292
|
+
fn prompt(&mut self, prompt: &str) -> Result<Option<String>, std::io::Error> {
|
287
293
|
let mut result = String::new();
|
288
294
|
loop {
|
289
295
|
self.status_message = StatusMessage::from(format!("{}{}", prompt, result));
|
290
296
|
self.refresh_screen()?;
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
297
|
+
match Terminal::read_key()? {
|
298
|
+
Key::Backspace => {
|
299
|
+
if !result.is_empty() {
|
300
|
+
result.truncate(result.len() - 1);
|
301
|
+
}
|
295
302
|
}
|
296
|
-
|
297
|
-
|
303
|
+
Key::Char('\n') => break,
|
304
|
+
Key::Char(c) => {
|
305
|
+
if !c.is_control() {
|
306
|
+
result.push(c);
|
307
|
+
}
|
308
|
+
}
|
309
|
+
Key::Esc => {
|
310
|
+
result.truncate(0);
|
311
|
+
break;
|
298
312
|
}
|
313
|
+
_ => (),
|
299
314
|
}
|
300
315
|
}
|
301
|
-
|
316
|
+
self.status_message = StatusMessage::from(String::new());
|
317
|
+
if result.is_empty() {
|
318
|
+
return Ok(None);
|
319
|
+
}
|
320
|
+
Ok(Some(result))
|
302
321
|
}
|
303
322
|
}
|
304
323
|
|
We have changed a couple of things here, let’s go through them one by one.
prompt
no longer only contains a Result
, but also an Option
. The idea is
that if the prompt is successful, it can still return None
to indicate that
the user has aborted the prompt. We have traded our if let
for a match
to
also handle the cases of backspace and Esc
: In the case of Esc
, we reset all
previously entered text before breaking the loop. In case of Backspace
, we
reduce the input by one, removing the last character in the progress.
Then, we have created a function save
outside of process_keypress
. In it, we
are now aborting the save operation if the prompt has returned None
, but also
if the prompt has returned an error.
Dirty flag
We’d like to keep track of whether the text loaded in our editor differs from what’s in the file. Then we can warn the user that they might lose unsaved changes when they try to quit.
We call a Document
“dirty” if it has been modified since opening or saving the
file. Let’s add a dirty
variable to the Document
and initialize it with
false
. We don’t want this to be modified from the outside, so we add a
read-only is_dirty
function to Document
. We’re also setting it to true
on
any text change, and to false
on save
.
@@ -7,6 +7,7 @@ use std::io::{Error, Write};
|
|
7
7
|
pub struct Document {
|
8
8
|
rows: Vec<Row>,
|
9
9
|
pub file_name: Option<String>,
|
10
|
+
dirty: bool,
|
10
11
|
}
|
11
12
|
|
12
13
|
impl Document {
|
@@ -19,6 +20,7 @@ impl Document {
|
|
19
20
|
Ok(Self {
|
20
21
|
rows,
|
21
22
|
file_name: Some(filename.to_string()),
|
23
|
+
dirty: false,
|
22
24
|
})
|
23
25
|
}
|
24
26
|
pub fn row(&self, index: usize) -> Option<&Row> {
|
@@ -31,9 +33,6 @@ impl Document {
|
|
31
33
|
self.rows.len()
|
32
34
|
}
|
33
35
|
fn insert_newline(&mut self, at: &Position) {
|
34
|
-
if at.y > self.len() {
|
35
|
-
return;
|
36
|
-
}
|
37
36
|
if at.y == self.len() {
|
38
37
|
self.rows.push(Row::default());
|
39
38
|
return;
|
@@ -42,6 +41,10 @@ impl Document {
|
|
42
41
|
self.rows.insert(at.y + 1, new_row);
|
43
42
|
}
|
44
43
|
pub fn insert(&mut self, at: &Position, c: char) {
|
44
|
+
if at.y > self.len() {
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
self.dirty = true;
|
45
48
|
if c == '\n' {
|
46
49
|
self.insert_newline(at);
|
47
50
|
return;
|
@@ -50,7 +53,7 @@ impl Document {
|
|
50
53
|
let mut row = Row::default();
|
51
54
|
row.insert(0, c);
|
52
55
|
self.rows.push(row);
|
53
|
-
} else
|
56
|
+
} else {
|
54
57
|
let row = self.rows.get_mut(at.y).unwrap();
|
55
58
|
row.insert(at.x, c);
|
56
59
|
}
|
@@ -60,6 +63,7 @@ impl Document {
|
|
60
63
|
if at.y >= len {
|
61
64
|
return;
|
62
65
|
}
|
66
|
+
self.dirty = true;
|
63
67
|
if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 {
|
64
68
|
let next_row = self.rows.remove(at.y + 1);
|
65
69
|
let row = self.rows.get_mut(at.y).unwrap();
|
@@ -69,14 +73,18 @@ impl Document {
|
|
69
73
|
row.delete(at.x);
|
70
74
|
}
|
71
75
|
}
|
72
|
-
pub fn save(&self) -> Result<(), Error> {
|
76
|
+
pub fn save(&mut self) -> Result<(), Error> {
|
73
77
|
if let Some(file_name) = &self.file_name {
|
74
78
|
let mut file = fs::File::create(file_name)?;
|
75
79
|
for row in &self.rows {
|
76
80
|
file.write_all(row.as_bytes())?;
|
77
81
|
file.write_all(b"\n")?;
|
78
82
|
}
|
83
|
+
self.dirty = false;
|
79
84
|
}
|
80
85
|
Ok(())
|
81
86
|
}
|
87
|
+
pub fn is_dirty(&self) -> bool {
|
88
|
+
self.dirty
|
89
|
+
}
|
82
90
|
}
|
@@ -256,12 +256,23 @@ impl Editor {
|
|
256
256
|
fn draw_status_bar(&self) {
|
257
257
|
let mut status;
|
258
258
|
let width = self.terminal.size().width as usize;
|
259
|
+
let modified_indicator = if self.document.is_dirty() {
|
260
|
+
" (modified)"
|
261
|
+
} else {
|
262
|
+
""
|
263
|
+
};
|
264
|
+
|
259
265
|
let mut file_name = "[No Name]".to_string();
|
260
266
|
if let Some(name) = &self.document.file_name {
|
261
267
|
file_name = name.clone();
|
262
268
|
file_name.truncate(20);
|
263
269
|
}
|
264
|
-
status = format!(
|
270
|
+
status = format!(
|
271
|
+
"{} - {} lines{}",
|
272
|
+
file_name,
|
273
|
+
self.document.len(),
|
274
|
+
modified_indicator
|
275
|
+
);
|
265
276
|
|
266
277
|
let line_indicator = format!(
|
267
278
|
"{}/{}",
|
Perhaps the only surprising change is that we rearranged the bounds handling in
insert
a bit. It’s now similar to the checking in delete
.
Quit confirmation
Now we’re ready to warn the user about unsaved changes when they try to quit. If
document.is_dirty()
is true, we will display a warning in the status bar, and
require the user to press Ctrl-Q three more times in order to quit without
saving.
@@ -10,6 +10,7 @@ use termion::event::Key;
|
|
10
10
|
const STATUS_FG_COLOR: color::Rgb = color::Rgb(63, 63, 63);
|
11
11
|
const STATUS_BG_COLOR: color::Rgb = color::Rgb(239, 239, 239);
|
12
12
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
13
|
+
const QUIT_TIMES: u8 = 3;
|
13
14
|
|
14
15
|
#[derive(Default)]
|
15
16
|
pub struct Position {
|
@@ -37,6 +38,7 @@ pub struct Editor {
|
|
37
38
|
offset: Position,
|
38
39
|
document: Document,
|
39
40
|
status_message: StatusMessage,
|
41
|
+
quit_times: u8,
|
40
42
|
}
|
41
43
|
|
42
44
|
impl Editor {
|
@@ -76,6 +78,7 @@ impl Editor {
|
|
76
78
|
cursor_position: Position::default(),
|
77
79
|
offset: Position::default(),
|
78
80
|
status_message: StatusMessage::from(initial_status),
|
81
|
+
quit_times: QUIT_TIMES,
|
79
82
|
}
|
80
83
|
}
|
81
84
|
|
@@ -116,7 +119,17 @@ impl Editor {
|
|
116
119
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
117
120
|
let pressed_key = Terminal::read_key()?;
|
118
121
|
match pressed_key {
|
119
|
-
Key::Ctrl('q') =>
|
122
|
+
Key::Ctrl('q') => {
|
123
|
+
if self.quit_times > 0 && self.document.is_dirty() {
|
124
|
+
self.status_message = StatusMessage::from(format!(
|
125
|
+
"WARNING! File has unsaved changes. Press Ctrl-Q {} more times to quit.",
|
126
|
+
self.quit_times
|
127
|
+
));;
|
128
|
+
self.quit_times -= 1;
|
129
|
+
return Ok(());
|
130
|
+
}
|
131
|
+
self.should_quit = true
|
132
|
+
}
|
120
133
|
Key::Ctrl('s') => self.save(),
|
121
134
|
Key::Char(c) => {
|
122
135
|
self.document.insert(&self.cursor_position, c);
|
@@ -140,6 +153,10 @@ impl Editor {
|
|
140
153
|
_ => (),
|
141
154
|
}
|
142
155
|
self.scroll();
|
156
|
+
if self.quit_times < QUIT_TIMES {
|
157
|
+
self.quit_times = QUIT_TIMES;
|
158
|
+
self.status_message = StatusMessage::from(String::new());
|
159
|
+
}
|
143
160
|
Ok(())
|
144
161
|
}
|
145
162
|
fn scroll(&mut self) {
|
We have added a new constant for the additional times we require the user to
press Ctrl-Q, and we use it as an additional field in Editor
. When
the document is dirty and the user attempts to quit, we count down quit_times
until it reaches 0
- then we finally quit. Note that we are returning within
the match
arm for quit. That way, the code after the match
is only called if
the user pressed another key than Ctrl-Q, so we can check after the
match
if quit_times
has been modified and reset it if necessary.
Final touches
Congratulations, you have built a text editor! But before we move on and add
more functionality in it, let’s check if we really covered our bases. Earlier in
this tutorial, we cared about overflows and saturated_adds
and so on, but are
we really prepared to handle larger files, or will hecto
panic? Also, since
Rust is all about performance, does our code perform well enough?
First, let’s teach Clippy a few new tricks.
@@ -1,4 +1,4 @@
|
|
1
|
-
#![warn(clippy::all, clippy::pedantic)]
|
1
|
+
#![warn(clippy::all, clippy::pedantic, clippy::restriction)]
|
2
2
|
mod document;
|
3
3
|
mod editor;
|
4
4
|
mod row;
|
clippy::restriction
contains a lot of warnings that might or might not
indicate errors in your code. As you can see when you run cargo clippy
now,
the results are overwhelming!
Lucky for us, each entry comes with a link and with that link come some
explanations. Let’s disable a few items for hecto
:
@@ -1,4 +1,12 @@
|
|
1
1
|
#![warn(clippy::all, clippy::pedantic, clippy::restriction)]
|
2
|
+
#![allow(
|
3
|
+
clippy::missing_docs_in_private_items,
|
4
|
+
clippy::implicit_return,
|
5
|
+
clippy::shadow_reuse,
|
6
|
+
clippy::print_stdout,
|
7
|
+
clippy::wildcard_enum_match_arm,
|
8
|
+
clippy::else_if_without_else
|
9
|
+
)]
|
2
10
|
mod document;
|
3
11
|
mod editor;
|
4
12
|
mod row;
|
If you are interested in what these options are, check their descriptions in the original output of clippy.
The results of Clippy are now much more manageable. There are still many entries regarding integer arithmetic. Before we get to them, let’s ask ourselves: Do we really want to fix all of them? My opinion is: Yes, for two reasons. One is that some of our code relies on implicit contracts behind some functions: We rely other portions of our code to do the checking for us so that we don’t have to. But what if in the future the other part of the code changes?
Another consideration is that if you come across code like a+1;
, you have to
stop and investigate the surrounding code to check if this operation is valid.
You have no indication whether or not the author of this code (which could
simply be your Past Self) has paid attention to the potential overflow or not!
The easiest thing to do is to disable clippy at this line. Even this lazy
solution is an indicator for everyone reviewing the code later that you did, in
fact, take overflows into account and made a conscious decision on how to deal
with it.
Anyways, let’s jump right in!
Making Clippy happy
@@ -33,11 +33,15 @@ impl Document {
|
|
33
33
|
self.rows.len()
|
34
34
|
}
|
35
35
|
fn insert_newline(&mut self, at: &Position) {
|
36
|
+
if at.y > self.len() {
|
37
|
+
return;
|
38
|
+
}
|
36
39
|
if at.y == self.len() {
|
37
40
|
self.rows.push(Row::default());
|
38
41
|
return;
|
39
42
|
}
|
40
43
|
let new_row = self.rows.get_mut(at.y).unwrap().split(at.x);
|
44
|
+
#[allow(clippy::integer_arithmetic)]
|
41
45
|
self.rows.insert(at.y + 1, new_row);
|
42
46
|
}
|
43
47
|
pub fn insert(&mut self, at: &Position, c: char) {
|
@@ -58,13 +62,14 @@ impl Document {
|
|
58
62
|
row.insert(at.x, c);
|
59
63
|
}
|
60
64
|
}
|
65
|
+
#[allow(clippy::integer_arithmetic)]
|
61
66
|
pub fn delete(&mut self, at: &Position) {
|
62
67
|
let len = self.len();
|
63
68
|
if at.y >= len {
|
64
69
|
return;
|
65
70
|
}
|
66
71
|
self.dirty = true;
|
67
|
-
if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len
|
72
|
+
if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y + 1< len {
|
68
73
|
let next_row = self.rows.remove(at.y + 1);
|
69
74
|
let row = self.rows.get_mut(at.y).unwrap();
|
70
75
|
row.append(&next_row);
|
@@ -213,14 +213,14 @@ impl Editor {
|
|
213
213
|
}
|
214
214
|
Key::PageUp => {
|
215
215
|
y = if y > terminal_height {
|
216
|
-
y
|
216
|
+
y.saturating_sub(terminal_height)
|
217
217
|
} else {
|
218
218
|
0
|
219
219
|
}
|
220
220
|
}
|
221
221
|
Key::PageDown => {
|
222
222
|
y = if y.saturating_add(terminal_height) < height {
|
223
|
-
y
|
223
|
+
y.saturating_add(terminal_height)
|
224
224
|
} else {
|
225
225
|
height
|
226
226
|
}
|
@@ -253,7 +253,7 @@ impl Editor {
|
|
253
253
|
pub fn draw_row(&self, row: &Row) {
|
254
254
|
let width = self.terminal.size().width as usize;
|
255
255
|
let start = self.offset.x;
|
256
|
-
let end = self.offset.x
|
256
|
+
let end = self.offset.x.saturating_add(width);
|
257
257
|
let row = row.render(start, end);
|
258
258
|
println!("{}\r", row)
|
259
259
|
}
|
@@ -261,7 +261,7 @@ impl Editor {
|
|
261
261
|
let height = self.terminal.size().height;
|
262
262
|
for terminal_row in 0..height {
|
263
263
|
Terminal::clear_current_line();
|
264
|
-
if let Some(row) = self.document.row(terminal_row as usize
|
264
|
+
if let Some(row) = self.document.row(self.offset.y.saturating_add(terminal_row as usize)) {
|
265
265
|
self.draw_row(row);
|
266
266
|
} else if self.document.is_empty() && terminal_row == height / 3 {
|
267
267
|
self.draw_welcome_message();
|
@@ -296,10 +296,9 @@ impl Editor {
|
|
296
296
|
self.cursor_position.y.saturating_add(1),
|
297
297
|
self.document.len()
|
298
298
|
);
|
299
|
+
#[allow(clippy::integer_arithmetic)]
|
299
300
|
let len = status.len() + line_indicator.len();
|
300
|
-
|
301
|
+
status.push_str(&" ".repeat(width.saturating_sub(len)));
|
301
|
-
status.push_str(&" ".repeat(width - len));
|
302
|
-
}
|
303
302
|
status = format!("{}{}", status, line_indicator);
|
304
303
|
status.truncate(width);
|
305
304
|
Terminal::set_bg_color(STATUS_BG_COLOR);
|
@@ -323,11 +322,7 @@ impl Editor {
|
|
323
322
|
self.status_message = StatusMessage::from(format!("{}{}", prompt, result));
|
324
323
|
self.refresh_screen()?;
|
325
324
|
match Terminal::read_key()? {
|
326
|
-
Key::Backspace =>
|
325
|
+
Key::Backspace => result.truncate(result.len().saturating_sub(1)),
|
327
|
-
if !result.is_empty() {
|
328
|
-
result.truncate(result.len() - 1);
|
329
|
-
}
|
330
|
-
}
|
331
326
|
Key::Char('\n') => break,
|
332
327
|
Key::Char(c) => {
|
333
328
|
if !c.is_control() {
|
@@ -23,6 +23,7 @@ impl Row {
|
|
23
23
|
let end = cmp::min(end, self.string.len());
|
24
24
|
let start = cmp::min(start, end);
|
25
25
|
let mut result = String::new();
|
26
|
+
#[allow(clippy::integer_arithmetic)]
|
26
27
|
for grapheme in self.string[..]
|
27
28
|
.graphemes(true)
|
28
29
|
.skip(start)
|
@@ -57,6 +58,7 @@ impl Row {
|
|
57
58
|
}
|
58
59
|
self.update_len();
|
59
60
|
}
|
61
|
+
#[allow(clippy::integer_arithmetic)]
|
60
62
|
pub fn delete(&mut self, at: usize) {
|
61
63
|
if at >= self.len() {
|
62
64
|
return;
|
As you can see, these are mainly small changes. I want to point out a few things:
- We have found several potential bugs or head scratchers. For instance,
after one of our last changes,
insert_newline
did not do some bounds checking on its own. From looking atinsert_newline
alone it would not be possible to understand that we removed it because right now,insert_newline
is only called frominsert
, where we are already doing bounds checking. This means that there was an implicit contract to whoever callsinsert_newline
to make sureat.y
is not exceeding the current document’s length. We have now corrected this. - We have replaced a subtraction from
len
with an addition toat.y
in another place. Why? Because we can easily see in that function thaty
will always be smaller thanlen
, so there is always room for a+1
. It’s not as easily visible that, or if,len
will always be greater than 0. - While using
saturating_sub
, we were able to get rid of some size comparisons, which simplifies our code.
Clippy still gives us some warnings, this time about integer division. The
problem is the following: If you divide, for example, 100/3
, the result will
be 33
, and the remainder will be removed. That is OK in our case, but the
reason to tackle this anyways is the same as before: Anybody reviewing our code
can’t be sure whether or not we thought about this, or simply forgot. The least
we can do is either leave a comment or a clippy directive, which is essentially
the same as a comment saying “Trust me, this stuff is working”.
@@ -244,6 +244,7 @@ impl Editor {
|
|
244
244
|
let mut welcome_message = format!("Hecto editor -- version {}", VERSION);
|
245
245
|
let width = self.terminal.size().width as usize;
|
246
246
|
let len = welcome_message.len();
|
247
|
+
#[allow(clippy::integer_arithmetic, clippy::integer_division)]
|
247
248
|
let padding = width.saturating_sub(len) / 2;
|
248
249
|
let spaces = " ".repeat(padding.saturating_sub(1));
|
249
250
|
welcome_message = format!("~{}{}", spaces, welcome_message);
|
@@ -257,11 +258,15 @@ impl Editor {
|
|
257
258
|
let row = row.render(start, end);
|
258
259
|
println!("{}\r", row)
|
259
260
|
}
|
261
|
+
#[allow(clippy::integer_division, clippy::integer_arithmetic)]
|
260
262
|
fn draw_rows(&self) {
|
261
263
|
let height = self.terminal.size().height;
|
262
264
|
for terminal_row in 0..height {
|
263
265
|
Terminal::clear_current_line();
|
264
|
-
if let Some(row) = self
|
266
|
+
if let Some(row) = self
|
267
|
+
.document
|
268
|
+
.row(self.offset.y.saturating_add(terminal_row as usize))
|
269
|
+
{
|
265
270
|
self.draw_row(row);
|
266
271
|
} else if self.document.is_empty() && terminal_row == height / 3 {
|
267
272
|
self.draw_welcome_message();
|
As you can see, we opted for the “Leave a comment” solution here. We also re-indented some code, as the lines tend to get longer now. Let’s tackle clippy’s next grievances now.
@@ -33,19 +33,20 @@ impl Document {
|
|
33
33
|
self.rows.len()
|
34
34
|
}
|
35
35
|
fn insert_newline(&mut self, at: &Position) {
|
36
|
-
if at.y > self.len() {
|
36
|
+
if at.y > self.rows.len() {
|
37
37
|
return;
|
38
38
|
}
|
39
|
-
if at.y == self.len() {
|
39
|
+
if at.y == self.rows.len() {
|
40
40
|
self.rows.push(Row::default());
|
41
41
|
return;
|
42
42
|
}
|
43
|
-
|
43
|
+
#[allow(clippy::indexing_slicing)]
|
44
|
+
let new_row = self.rows[at.y].split(at.x);
|
44
45
|
#[allow(clippy::integer_arithmetic)]
|
45
46
|
self.rows.insert(at.y + 1, new_row);
|
46
47
|
}
|
47
48
|
pub fn insert(&mut self, at: &Position, c: char) {
|
48
|
-
if at.y > self.len() {
|
49
|
+
if at.y > self.rows.len() {
|
49
50
|
return;
|
50
51
|
}
|
51
52
|
self.dirty = true;
|
@@ -53,28 +54,29 @@ impl Document {
|
|
53
54
|
self.insert_newline(at);
|
54
55
|
return;
|
55
56
|
}
|
56
|
-
if at.y == self.len() {
|
57
|
+
if at.y == self.rows.len() {
|
57
58
|
let mut row = Row::default();
|
58
59
|
row.insert(0, c);
|
59
60
|
self.rows.push(row);
|
60
61
|
} else {
|
61
|
-
|
62
|
+
#[allow(clippy::indexing_slicing)]
|
63
|
+
let row = &mut self.rows[at.y];
|
62
64
|
row.insert(at.x, c);
|
63
65
|
}
|
64
66
|
}
|
65
|
-
#[allow(clippy::integer_arithmetic)]
|
67
|
+
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
66
68
|
pub fn delete(&mut self, at: &Position) {
|
67
|
-
let len = self.len();
|
69
|
+
let len = self.rows.len();
|
68
70
|
if at.y >= len {
|
69
71
|
return;
|
70
72
|
}
|
71
73
|
self.dirty = true;
|
72
|
-
if at.x == self.rows
|
74
|
+
if at.x == self.rows[at.y].len() && at.y + 1 < len {
|
73
75
|
let next_row = self.rows.remove(at.y + 1);
|
74
|
-
let row = self.rows
|
76
|
+
let row = &mut self.rows[at.y];
|
75
77
|
row.append(&next_row);
|
76
78
|
} else {
|
77
|
-
let row = self.rows
|
79
|
+
let row = &mut self.rows[at.y];
|
78
80
|
row.delete(at.x);
|
79
81
|
}
|
80
82
|
}
|
This change is mainly related to accessing rows at certain positions. We used to
use the safe method get_mut
, which does not panic, even if there was nothing
to retrieve, e.g. because the index was wrong. And we called unwrap()
directly
on it, negating the benefits of using get_mut
in the first place. We have
replaced it now with a direct access to self.rows
. We have left a clippy
statement everywhere we did this, to indicate that we did, in fact, check that
we’re only accessing valid indices at that time.
If you wanted to make your code even more robust, you could replace these occurrences with proper handling in case an index is out of bounds.
We also changed another thing: There was another implicit contract thing going
on, and that was that the length of a document always corresponds to the number
of rows in it, so we were calling self.len()
instead of self.rows.len()
everywhere. Should we ever decide that documents can be longer, all our
operations on self.row
would fail.
This is not a super important change, but it fits the spirit of our current refactorings. All right, two more clippy warnings to go.
@@ -58,11 +58,11 @@ impl Editor {
|
|
58
58
|
pub fn default() -> Self {
|
59
59
|
let args: Vec<String> = env::args().collect();
|
60
60
|
let mut initial_status = String::from("HELP: Ctrl-S = save | Ctrl-Q = quit");
|
61
|
-
|
62
|
-
|
63
|
-
let doc = Document::open(
|
64
|
-
if doc
|
65
|
-
doc
|
61
|
+
|
62
|
+
let document = if let Some(file_name) = args.get(1) {
|
63
|
+
let doc = Document::open(file_name);
|
64
|
+
if let Ok(doc) = doc {
|
65
|
+
doc
|
66
66
|
} else {
|
67
67
|
initial_status = format!("ERR: Could not open file: {}", file_name);
|
68
68
|
Document::default()
|
We’re taking advantage of the fact that get
, as discussed above, only returns
a value to us if it is there, eliminating the need to perform a check before
indexing. Then, we remove is_ok
in favor of an if_let
, to save an unrwap
.
Finally, we have convinced both clippy and ourselves that our code is good!
Now, there are a few things that Clippy can’t detect, and we should not rely on Clippy alone for our development. We’ll deal with them next.
Performance improvements
So far, our editor does not do much. But there are already some performance
improvements that we could make! Performance tweaks are a difficult topic, as
it’s hard to draw the line between readable, maintainable code and
super-optimized code that is hard to read and maintain. We don’t want hecto
to
be the fastest editor out there, but it makes sense to take a look at a few
performance aspects.
I want us to look out for unnecessary iterations over rows when going through the document from top to bottom, as well as unnecessary iterations over characters as we go through a row left to right. That’s all that we are going to focus on now - no additional caching, no fancy tricks, just looking for redundant operations.
Let’s focus on how we deal with rows. We have a common pattern that we are
repeating multiple times. For instance, here’s insert
:
pub fn insert(&mut self, at: usize, c: char) {
if at >= self.len() {
self.string.push(c);
} else {
let mut result: String = self.string[..].graphemes(true).take(at).collect();
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
result.push(c);
result.push_str(&remainder);
self.string = result;
}
self.update_len();
}
In this implementation, we iterate over our string three times:
- Once from start to
at
to calculateresult
- A second time from start to end (ignoring everything between start and
at
) to calculateremainder
- and once through the whole string to update
len
.
That’s not great. Let’s change this.
@@ -9,12 +9,10 @@ pub struct Row {
|
|
9
9
|
|
10
10
|
impl From<&str> for Row {
|
11
11
|
fn from(slice: &str) -> Self {
|
12
|
-
|
12
|
+
Self {
|
13
13
|
string: String::from(slice),
|
14
|
-
len:
|
15
|
-
}
|
14
|
+
len: slice.graphemes(true).count(),
|
15
|
+
}
|
16
|
-
row.update_len();
|
17
|
-
row
|
18
16
|
}
|
19
17
|
}
|
20
18
|
|
@@ -43,43 +41,65 @@ impl Row {
|
|
43
41
|
pub fn is_empty(&self) -> bool {
|
44
42
|
self.len == 0
|
45
43
|
}
|
46
|
-
fn update_len(&mut self) {
|
47
|
-
self.len = self.string[..].graphemes(true).count();
|
48
|
-
}
|
49
44
|
pub fn insert(&mut self, at: usize, c: char) {
|
50
45
|
if at >= self.len() {
|
51
46
|
self.string.push(c);
|
52
|
-
|
53
|
-
|
47
|
+
self.len += 1;
|
48
|
+
return;
|
54
|
-
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
|
55
|
-
result.push(c);
|
56
|
-
result.push_str(&remainder);
|
57
|
-
self.string = result;
|
58
49
|
}
|
59
|
-
|
50
|
+
let mut result: String = String::new();
|
51
|
+
let mut length = 0;
|
52
|
+
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
53
|
+
length += 1;
|
54
|
+
if index == at {
|
55
|
+
length += 1;
|
56
|
+
result.push(c);
|
57
|
+
}
|
58
|
+
result.push_str(grapheme);
|
59
|
+
}
|
60
|
+
self.len = length;
|
61
|
+
self.string = result;
|
60
62
|
}
|
61
|
-
#[allow(clippy::integer_arithmetic)]
|
62
63
|
pub fn delete(&mut self, at: usize) {
|
63
64
|
if at >= self.len() {
|
64
65
|
return;
|
65
|
-
} else {
|
66
|
-
let mut result: String = self.string[..].graphemes(true).take(at).collect();
|
67
|
-
let remainder: String = self.string[..].graphemes(true).skip(at + 1).collect();
|
68
|
-
result.push_str(&remainder);
|
69
|
-
self.string = result;
|
70
66
|
}
|
71
|
-
|
67
|
+
let mut result: String = String::new();
|
68
|
+
let mut length = 0;
|
69
|
+
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
70
|
+
if index != at {
|
71
|
+
length += 1;
|
72
|
+
result.push_str(grapheme);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
self.len = length;
|
76
|
+
self.string = result;
|
72
77
|
}
|
73
78
|
pub fn append(&mut self, new: &Self) {
|
74
79
|
self.string = format!("{}{}", self.string, new.string);
|
75
|
-
self.
|
80
|
+
self.len += new.len;
|
76
81
|
}
|
77
82
|
pub fn split(&mut self, at: usize) -> Self {
|
78
|
-
let
|
79
|
-
let
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
+
let mut row: String = String::new();
|
84
|
+
let mut length = 0;
|
85
|
+
let mut splitted_row: String = String::new();
|
86
|
+
let mut splitted_length = 0;
|
87
|
+
for (index, grapheme) in self.string[..].graphemes(true).enumerate() {
|
88
|
+
if index < at {
|
89
|
+
length += 1;
|
90
|
+
row.push_str(grapheme);
|
91
|
+
} else {
|
92
|
+
splitted_length += 1;
|
93
|
+
splitted_row.push_str(grapheme);
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
self.string = row;
|
98
|
+
self.len = length;
|
99
|
+
Self {
|
100
|
+
string: splitted_row,
|
101
|
+
len: splitted_length,
|
102
|
+
}
|
83
103
|
}
|
84
104
|
pub fn as_bytes(&self) -> &[u8] {
|
85
105
|
self.string.as_bytes()
|
We did two things here:
- We got rid of
update_len
, as we are now manually calculating the length on every row operation. - We are looping over
enumerate
, which does not only provide us with the next element, but also with it’s index in the iterator. That way, we can easily calculate the length while we are moving through the row.
Final considerations
While we undoubtedly have made hecto
better with these changes, let’s put
things a bit into perspective: How likely is it that we will ever see an
overflow happening on usize
? Well, that depends on your system. You can check
the actual size of usize
with the following snippet:
fn main() {
dbg!(std::usize::MAX);
}
On my machine, it outputs the following:
[src/main.rs:2] std::usize::MAX = 18446744073709551615
That means, we are near an overflow as soon as our doc is close to 18,446,744,073,709,551,615 rows, or if a row has close to 18,446,744,073,709,551,615 characters long.
That is an insanely large number. If every row contained a byte of information,
that would be 18 EB of data. EB stands for Exabyte. Good luck finding a hard
drive that can handle these data! And even if you could, hecto
would run into
other issues while handling this insane amount of data.
This does not mean that our work was not important. On the contrary, I believe that thinking about these kinds of things should become a habit while you are coding. However, you should not over-optimize your code for an edge case that will never happen.
Conclusion
You have now successfully built a text editor. If you are brave, you can use
hecto
to work on hecto
. In the next chapter, we will make use of prompt()
to
implement an incremental search feature in our editor.