Previous chapter - Overview - Appendices
We are almost done with our text editor - we’re only missing some syntax highlighting.
Colorful Digits
Let’s start by just getting some color on the screen, as simply as possible. We’ll attempt to highlight numbers by coloring each digit character red.
@@ -1,5 +1,6 @@
|
|
1
1
|
use crate::SearchDirection;
|
2
2
|
use std::cmp;
|
3
|
+
use termion::color;
|
3
4
|
use unicode_segmentation::UnicodeSegmentation;
|
4
5
|
|
5
6
|
#[derive(Default)]
|
@@ -28,10 +29,21 @@ impl Row {
|
|
28
29
|
.skip(start)
|
29
30
|
.take(end - start)
|
30
31
|
{
|
31
|
-
if
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
if let Some(c) = grapheme.chars().next() {
|
33
|
+
if c == '\t' {
|
34
|
+
result.push_str(" ");
|
35
|
+
} else if c.is_ascii_digit() {
|
36
|
+
result.push_str(
|
37
|
+
&format!(
|
38
|
+
"{}{}{}",
|
39
|
+
termion::color::Fg(color::Rgb(220, 163, 163)),
|
40
|
+
c,
|
41
|
+
color::Fg(color::Reset)
|
42
|
+
)[..],
|
43
|
+
);
|
44
|
+
} else {
|
45
|
+
result.push(c);
|
46
|
+
}
|
35
47
|
}
|
36
48
|
}
|
37
49
|
result
|
We have now converted our grapheme into a character, so that we can use
is_ascii_digit
- which determines whether or not a character is a digit. If it
is, we change the foreground color to a shade of
red and reset it immediately afterwards.
Refactor syntax highlighting
Now we know how to color text, but we’re going to have to do a lot more work to
actually highlight entire strings, keywords, comments, and so on. We can’t just
decide what color to use based on the class of each character, like we’re doing
with digits currently. What we want to do is figure out the highlighting for
each row of text before we display it, and then re-highlight a line whenever it
gets changed. What makes things more complicated is that the highlighting
depends on characters currently out of view - for instance, if a String
starts
to the left of the currently visible portion of the row, we want to treat a "
on screen as the end of a string, and not as the start of one. Our current
strategy to look at each visible character is therefore not sufficient.
Instead, we are going to store the highlighting of each character of a row in a vector. Let’s start by adding an enum which will hold our different highlighting types as well as a vector to hold them.
@@ -0,0 +1,4 @@
|
|
1
|
+
pub enum Type {
|
2
|
+
None,
|
3
|
+
Number,
|
4
|
+
}
|
@@ -9,6 +9,7 @@
|
|
9
9
|
)]
|
10
10
|
mod document;
|
11
11
|
mod editor;
|
12
|
+
mod highlighting;
|
12
13
|
mod row;
|
13
14
|
mod terminal;
|
14
15
|
pub use document::Document;
|
@@ -1,3 +1,4 @@
|
|
1
|
+
use crate::highlighting;
|
1
2
|
use crate::SearchDirection;
|
2
3
|
use std::cmp;
|
3
4
|
use termion::color;
|
@@ -6,6 +7,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|
6
7
|
#[derive(Default)]
|
7
8
|
pub struct Row {
|
8
9
|
string: String,
|
10
|
+
highlighting: Vec<highlighting::Type>,
|
9
11
|
len: usize,
|
10
12
|
}
|
11
13
|
|
@@ -13,6 +15,7 @@ impl From<&str> for Row {
|
|
13
15
|
fn from(slice: &str) -> Self {
|
14
16
|
Self {
|
15
17
|
string: String::from(slice),
|
18
|
+
highlighting: Vec::new(),
|
16
19
|
len: slice.graphemes(true).count(),
|
17
20
|
}
|
18
21
|
}
|
@@ -112,6 +115,7 @@ impl Row {
|
|
112
115
|
Self {
|
113
116
|
string: splitted_row,
|
114
117
|
len: splitted_length,
|
118
|
+
highlighting: Vec::new(),
|
115
119
|
}
|
116
120
|
}
|
117
121
|
pub fn as_bytes(&self) -> &[u8] {
|
Highlighting will be controlled by the Document
, the rows do not “highlight
themselves”. The reason will become clearer as we add more code - essentially,
to highlight a row, more information than what is present in the Row
is
needed. This means that any operation on the Row
from the outside can
potentially render the highlighting invalid. When developing functionality for
Row
, we are going to pay special attention so that the worst that can happen
is a wrong highlighting (as opposed to a crash).
This is why we are not setting the highlight for example in split
.
For now, we’ll focus on highlighting numbers only. So we want every character
that’s part of a number to have a corresponding Type::Number
value in the
highlighting
vector, and we want every other value in highlighting
to be
Type::None
.
Let’s create a new highlight
function in our row. This function will go
through the characters of the string
and highlight them by setting each value
in the highlighting
vector.
@@ -1,4 +1,4 @@
|
|
1
1
|
pub enum Type {
|
2
2
|
None,
|
3
3
|
Number,
|
4
|
-
}
|
4
|
+
}
|
@@ -158,4 +158,15 @@ impl Row {
|
|
158
158
|
}
|
159
159
|
None
|
160
160
|
}
|
161
|
+
pub fn highlight(&mut self) {
|
162
|
+
let mut highlighting = Vec::new();
|
163
|
+
for c in self.string.chars() {
|
164
|
+
if c.is_ascii_digit() {
|
165
|
+
highlighting.push(highlighting::Type::Number);
|
166
|
+
} else {
|
167
|
+
highlighting.push(highlighting::Type::None);
|
168
|
+
}
|
169
|
+
}
|
170
|
+
self.highlighting = highlighting;
|
171
|
+
}
|
161
172
|
}
|
The code of highlight
is straightforward: If a character is a digit, we push
Type::Number
, otherwise, we push Type::None
.
Now we want to have a function which maps the Type
to a color. Let’s implement
that as a method of Type
:
@@ -1,4 +1,14 @@
|
|
1
|
+
use termion::color;
|
1
2
|
pub enum Type {
|
2
3
|
None,
|
3
4
|
Number,
|
4
|
-
}
|
5
|
+
}
|
6
|
+
|
7
|
+
impl Type {
|
8
|
+
fn to_color(&self) -> impl color::Color {
|
9
|
+
match self {
|
10
|
+
Type::Number => color::Rgb(220, 163, 163),
|
11
|
+
_ => color::Rgb(255, 255, 255),
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
We are returning red now for numbers and white for all other cases. Now let’s finally draw the highlighted text to the screen!
@@ -16,7 +16,9 @@ impl Document {
|
|
16
16
|
let contents = fs::read_to_string(filename)?;
|
17
17
|
let mut rows = Vec::new();
|
18
18
|
for value in contents.lines() {
|
19
|
-
|
19
|
+
let mut row = Row::from(value);
|
20
|
+
row.highlight();
|
21
|
+
rows.push(row);
|
20
22
|
}
|
21
23
|
Ok(Self {
|
22
24
|
rows,
|
@@ -42,7 +44,10 @@ impl Document {
|
|
42
44
|
return;
|
43
45
|
}
|
44
46
|
#[allow(clippy::indexing_slicing)]
|
45
|
-
let
|
47
|
+
let current_row = &mut self.rows[at.y];
|
48
|
+
let mut new_row = current_row.split(at.x);
|
49
|
+
current_row.highlight();
|
50
|
+
new_row.highlight();
|
46
51
|
#[allow(clippy::integer_arithmetic)]
|
47
52
|
self.rows.insert(at.y + 1, new_row);
|
48
53
|
}
|
@@ -58,11 +63,13 @@ impl Document {
|
|
58
63
|
if at.y == self.rows.len() {
|
59
64
|
let mut row = Row::default();
|
60
65
|
row.insert(0, c);
|
66
|
+
row.highlight();
|
61
67
|
self.rows.push(row);
|
62
68
|
} else {
|
63
69
|
#[allow(clippy::indexing_slicing)]
|
64
70
|
let row = &mut self.rows[at.y];
|
65
71
|
row.insert(at.x, c);
|
72
|
+
row.highlight();
|
66
73
|
}
|
67
74
|
}
|
68
75
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
@@ -76,9 +83,11 @@ impl Document {
|
|
76
83
|
let next_row = self.rows.remove(at.y + 1);
|
77
84
|
let row = &mut self.rows[at.y];
|
78
85
|
row.append(&next_row);
|
86
|
+
row.highlight();
|
79
87
|
} else {
|
80
88
|
let row = &mut self.rows[at.y];
|
81
89
|
row.delete(at.x);
|
90
|
+
row.highlight();
|
82
91
|
}
|
83
92
|
}
|
84
93
|
pub fn save(&mut self) -> Result<(), Error> {
|
@@ -5,7 +5,7 @@ pub enum Type {
|
|
5
5
|
}
|
6
6
|
|
7
7
|
impl Type {
|
8
|
-
fn to_color(&self) -> impl color::Color {
|
8
|
+
pub fn to_color(&self) -> impl color::Color {
|
9
9
|
match self {
|
10
10
|
Type::Number => color::Rgb(220, 163, 163),
|
11
11
|
_ => color::Rgb(255, 255, 255),
|
@@ -27,26 +27,27 @@ impl Row {
|
|
27
27
|
let start = cmp::min(start, end);
|
28
28
|
let mut result = String::new();
|
29
29
|
#[allow(clippy::integer_arithmetic)]
|
30
|
-
for grapheme in self.string[..]
|
30
|
+
for (index, grapheme) in self.string[..]
|
31
31
|
.graphemes(true)
|
32
|
+
.enumerate()
|
32
33
|
.skip(start)
|
33
34
|
.take(end - start)
|
34
35
|
{
|
35
36
|
if let Some(c) = grapheme.chars().next() {
|
37
|
+
let highlighting_type = self
|
38
|
+
.highlighting
|
39
|
+
.get(index)
|
40
|
+
.unwrap_or(&highlighting::Type::None);
|
41
|
+
let start_highlight =
|
42
|
+
format!("{}", termion::color::Fg(highlighting_type.to_color()));
|
43
|
+
result.push_str(&start_highlight[..]);
|
36
44
|
if c == '\t' {
|
37
45
|
result.push_str(" ");
|
38
|
-
} else if c.is_ascii_digit() {
|
39
|
-
result.push_str(
|
40
|
-
&format!(
|
41
|
-
"{}{}{}",
|
42
|
-
termion::color::Fg(color::Rgb(220, 163, 163)),
|
43
|
-
c,
|
44
|
-
color::Fg(color::Reset)
|
45
|
-
)[..],
|
46
|
-
);
|
47
46
|
} else {
|
48
47
|
result.push(c);
|
49
48
|
}
|
49
|
+
let end_highlight = format!("{}", termion::color::Fg(color::Reset));
|
50
|
+
result.push_str(&end_highlight[..]);
|
50
51
|
}
|
51
52
|
}
|
52
53
|
result
|
First, we call highlight
everywhere in Document
where we modify a row. Then,
we refactor our rendering: We get the correct highlighting for the current index
(which we obtain by using enumerate
). We change the color, append to the
string which we return, and change the color back again.
This works, but do we really have to write out an escape sequence before every single character? In practice, most characters are going to be the same color as the previous character, so most of the escape sequences are redundant. Let’s keep track of the current text color as we loop through the characters, and only print out an escape sequence when the color changes.
@@ -1,4 +1,5 @@
|
|
1
1
|
use termion::color;
|
2
|
+
#[derive(PartialEq)]
|
2
3
|
pub enum Type {
|
3
4
|
None,
|
4
5
|
Number,
|
@@ -26,6 +26,7 @@ impl Row {
|
|
26
26
|
let end = cmp::min(end, self.string.len());
|
27
27
|
let start = cmp::min(start, end);
|
28
28
|
let mut result = String::new();
|
29
|
+
let mut current_highlighting = &highlighting::Type::None;
|
29
30
|
#[allow(clippy::integer_arithmetic)]
|
30
31
|
for (index, grapheme) in self.string[..]
|
31
32
|
.graphemes(true)
|
@@ -38,18 +39,21 @@ impl Row {
|
|
38
39
|
.highlighting
|
39
40
|
.get(index)
|
40
41
|
.unwrap_or(&highlighting::Type::None);
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
if highlighting_type != current_highlighting {
|
43
|
+
current_highlighting = highlighting_type;
|
44
|
+
let start_highlight =
|
45
|
+
format!("{}", termion::color::Fg(highlighting_type.to_color()));
|
46
|
+
result.push_str(&start_highlight[..]);
|
47
|
+
}
|
44
48
|
if c == '\t' {
|
45
49
|
result.push_str(" ");
|
46
50
|
} else {
|
47
51
|
result.push(c);
|
48
52
|
}
|
49
|
-
let end_highlight = format!("{}", termion::color::Fg(color::Reset));
|
50
|
-
result.push_str(&end_highlight[..]);
|
51
53
|
}
|
52
54
|
}
|
55
|
+
let end_highlight = format!("{}", termion::color::Fg(color::Reset));
|
56
|
+
result.push_str(&end_highlight[..]);
|
53
57
|
result
|
54
58
|
}
|
55
59
|
pub fn len(&self) -> usize {
|
We use current_highlighting
to keep track of what we are currently rendering.
When it changes, we add the color change to the render string. We have also
moved the ending of the highlighting outside of the loop, so that we reset the
color at the end of each line. To allow comparison between Type
s, we derive
PartialEq
again.
Colorful search results
Before we start highlighting strings and keywords and all that, let’s use our
highlighting system to highlight search results. We’ll start by adding Match
to the HighlightingType
enum, and mapping it to the color blue in to_color
.
@@ -3,12 +3,14 @@ use termion::color;
|
|
3
3
|
pub enum Type {
|
4
4
|
None,
|
5
5
|
Number,
|
6
|
+
Match,
|
6
7
|
}
|
7
8
|
|
8
9
|
impl Type {
|
9
10
|
pub fn to_color(&self) -> impl color::Color {
|
10
11
|
match self {
|
11
12
|
Type::Number => color::Rgb(220, 163, 163),
|
13
|
+
Type::Match => color::Rgb(38, 139, 210),
|
12
14
|
_ => color::Rgb(255, 255, 255),
|
13
15
|
}
|
14
16
|
}
|
Next, we want to change highlight
so that it accepts an optional word. If no
word is given, no match is highlighted.
@@ -17,7 +17,7 @@ impl Document {
|
|
17
17
|
let mut rows = Vec::new();
|
18
18
|
for value in contents.lines() {
|
19
19
|
let mut row = Row::from(value);
|
20
|
-
row.highlight();
|
20
|
+
row.highlight(None);
|
21
21
|
rows.push(row);
|
22
22
|
}
|
23
23
|
Ok(Self {
|
@@ -46,8 +46,8 @@ impl Document {
|
|
46
46
|
#[allow(clippy::indexing_slicing)]
|
47
47
|
let current_row = &mut self.rows[at.y];
|
48
48
|
let mut new_row = current_row.split(at.x);
|
49
|
-
current_row.highlight();
|
50
|
-
new_row.highlight();
|
49
|
+
current_row.highlight(None);
|
50
|
+
new_row.highlight(None);
|
51
51
|
#[allow(clippy::integer_arithmetic)]
|
52
52
|
self.rows.insert(at.y + 1, new_row);
|
53
53
|
}
|
@@ -63,13 +63,13 @@ impl Document {
|
|
63
63
|
if at.y == self.rows.len() {
|
64
64
|
let mut row = Row::default();
|
65
65
|
row.insert(0, c);
|
66
|
-
row.highlight();
|
66
|
+
row.highlight(None);
|
67
67
|
self.rows.push(row);
|
68
68
|
} else {
|
69
69
|
#[allow(clippy::indexing_slicing)]
|
70
70
|
let row = &mut self.rows[at.y];
|
71
71
|
row.insert(at.x, c);
|
72
|
-
row.highlight();
|
72
|
+
row.highlight(None);
|
73
73
|
}
|
74
74
|
}
|
75
75
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
@@ -83,11 +83,11 @@ impl Document {
|
|
83
83
|
let next_row = self.rows.remove(at.y + 1);
|
84
84
|
let row = &mut self.rows[at.y];
|
85
85
|
row.append(&next_row);
|
86
|
-
row.highlight();
|
86
|
+
row.highlight(None);
|
87
87
|
} else {
|
88
88
|
let row = &mut self.rows[at.y];
|
89
89
|
row.delete(at.x);
|
90
|
-
row.highlight();
|
90
|
+
row.highlight(None);
|
91
91
|
}
|
92
92
|
}
|
93
93
|
pub fn save(&mut self) -> Result<(), Error> {
|
@@ -140,4 +140,9 @@ impl Document {
|
|
140
140
|
}
|
141
141
|
None
|
142
142
|
}
|
143
|
+
pub fn highlight(&mut self, word: Option<&str>) {
|
144
|
+
for row in &mut self.rows {
|
145
|
+
row.highlight(word);
|
146
|
+
}
|
147
|
+
}
|
143
148
|
}
|
@@ -150,6 +150,7 @@ impl Editor {
|
|
150
150
|
} else if moved {
|
151
151
|
editor.move_cursor(Key::Left);
|
152
152
|
}
|
153
|
+
editor.document.highlight(Some(query));
|
153
154
|
},
|
154
155
|
)
|
155
156
|
.unwrap_or(None);
|
@@ -158,6 +159,7 @@ impl Editor {
|
|
158
159
|
self.cursor_position = old_position;
|
159
160
|
self.scroll();
|
160
161
|
}
|
162
|
+
self.document.highlight(None);
|
161
163
|
}
|
162
164
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
163
165
|
let pressed_key = Terminal::read_key()?;
|
@@ -127,7 +127,7 @@ impl Row {
|
|
127
127
|
self.string.as_bytes()
|
128
128
|
}
|
129
129
|
pub fn find(&self, query: &str, at: usize, direction: SearchDirection) -> Option<usize> {
|
130
|
-
if at > self.len {
|
130
|
+
if at > self.len || query.is_empty() {
|
131
131
|
return None;
|
132
132
|
}
|
133
133
|
let start = if direction == SearchDirection::Forward {
|
@@ -163,15 +163,44 @@ impl Row {
|
|
163
163
|
}
|
164
164
|
None
|
165
165
|
}
|
166
|
-
pub fn highlight(&mut self) {
|
166
|
+
pub fn highlight(&mut self, word: Option<&str>) {
|
167
167
|
let mut highlighting = Vec::new();
|
168
|
-
|
168
|
+
let chars: Vec<char> = self.string.chars().collect();
|
169
|
+
let mut matches = Vec::new();
|
170
|
+
let mut search_index = 0;
|
171
|
+
|
172
|
+
if let Some(word) = word {
|
173
|
+
while let Some(search_match) = self.find(word, search_index, SearchDirection::Forward) {
|
174
|
+
matches.push(search_match);
|
175
|
+
if let Some(next_index) = search_match.checked_add(word[..].graphemes(true).count())
|
176
|
+
{
|
177
|
+
search_index = next_index;
|
178
|
+
} else {
|
179
|
+
break;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
let mut index = 0;
|
185
|
+
while let Some(c) = chars.get(index) {
|
186
|
+
if let Some(word) = word {
|
187
|
+
if matches.contains(&index) {
|
188
|
+
for _ in word[..].graphemes(true) {
|
189
|
+
index += 1;
|
190
|
+
highlighting.push(highlighting::Type::Match);
|
191
|
+
}
|
192
|
+
continue;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
169
196
|
if c.is_ascii_digit() {
|
170
197
|
highlighting.push(highlighting::Type::Number);
|
171
198
|
} else {
|
172
199
|
highlighting.push(highlighting::Type::None);
|
173
200
|
}
|
201
|
+
index += 1;
|
174
202
|
}
|
203
|
+
|
175
204
|
self.highlighting = highlighting;
|
176
205
|
}
|
177
206
|
}
|
Before we investigate the changes in highlight
, let’s first focus on the other
changes: We allow an optional parameter to highlight
, which we provide as
None
everywhere we call highlight
. We then add a method called highlight
to the document, which triggers a highlighting of all rows in the doc.
We use that method during our search by passing the current query in, and None
as soon as the search has been aborted.
Now, what about highlight
?
First, we collect all the matches of the word we have in the current row. That is more efficient than performing the search on every character.
We are using two new concepts while doing so. One is while..let
, which is
similar to if..let
, but it loops as long as the condition is satisifed. We use
that to loop through our current row, advancing the starting point for our
search directly behind the last match on every turn.
We are also using a new method to add 1
: checked_add
. This function returns
an Option
with the result if no overflow ocurred, or None
otherwise. Why
can’t we use saturating_add
here? Well, let’s assume our match happens to be
the very last part of the row, and we are also at the end of usize
. Then we
can’t advance next_index
any further with saturating_add
, it would return
the same result over and over, the while
condition would never be false and we
would loop indefinitely.
Speaking of loops, we also changed our loop for processing characters. This allows us to consume multiple characters at a time. We use this to push multiple highlighting types at once while we are at the index of a match which we want to highlight.
Try it out, and you will see that all search results light up in your editor.
Colorful numbers
Alright, let’s start working on highlighting numbers properly.
Right now, numbers are highlighted even if they’re part of an identifier, such as
the 32 in u32
. To fix that, we’ll require that numbers are preceded by a
separator character, which includes whitespace or punctuation characters. We can
use is_ascii_punctuation
and is_ascii_whitespace
for that.
@@ -180,7 +180,7 @@ impl Row {
|
|
180
180
|
}
|
181
181
|
}
|
182
182
|
}
|
183
|
-
|
183
|
+
let mut prev_is_separator = true;
|
184
184
|
let mut index = 0;
|
185
185
|
while let Some(c) = chars.get(index) {
|
186
186
|
if let Some(word) = word {
|
@@ -192,12 +192,22 @@ impl Row {
|
|
192
192
|
continue;
|
193
193
|
}
|
194
194
|
}
|
195
|
-
|
196
|
-
|
195
|
+
let previous_highlight = if index > 0 {
|
196
|
+
#[allow(clippy::integer_arithmetic)]
|
197
|
+
highlighting
|
198
|
+
.get(index - 1)
|
199
|
+
.unwrap_or(&highlighting::Type::None)
|
200
|
+
} else {
|
201
|
+
&highlighting::Type::None
|
202
|
+
};
|
203
|
+
if c.is_ascii_digit()
|
204
|
+
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number)
|
205
|
+
{
|
197
206
|
highlighting.push(highlighting::Type::Number);
|
198
207
|
} else {
|
199
208
|
highlighting.push(highlighting::Type::None);
|
200
209
|
}
|
210
|
+
prev_is_separator = c.is_ascii_punctuation() || c.is_ascii_whitespace();
|
201
211
|
index += 1;
|
202
212
|
}
|
203
213
|
|
We’re adding a new variable prev_separator
to check if the last character we
saw was a valid separator, after which we want to highlight numbers properly,
or any other character, after which we don’t want to highlight numbers.
We are also accessing the previous highlighting so that even if we are not behind a separator, we continue highlighting numbers as numbers - which allows us to highlight numbers with more than one digit.
At the end of the loop, we set prev_is_separator
to true
if the current
character is either an ascii punctuation or a whitespace, otherwise it’s set to
false
. Then we increment index
to consume the character.
Now let’s support highlighting numbers that contain decimal points.
@@ -200,8 +200,9 @@ impl Row {
|
|
200
200
|
} else {
|
201
201
|
&highlighting::Type::None
|
202
202
|
};
|
203
|
-
if c.is_ascii_digit()
|
204
|
-
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number)
|
203
|
+
if (c.is_ascii_digit()
|
204
|
+
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number))
|
205
|
+
|| (c == &'.' && previous_highlight == &highlighting::Type::Number)
|
205
206
|
{
|
206
207
|
highlighting.push(highlighting::Type::Number);
|
207
208
|
} else {
|
A .
character that comes after a character that we just highlighted as a
number will now be considered part of the number.
Detect file type
Before we go on to highlight other things, we’re going to add filetype detection to our editor. This will allow us to have different rules for how to highlight different types of files. For example, text files shouldn’t have any highlighting, and Rust files should highlight numbers, strings, chars, comments and many keywords specific to Rust.
Let’s create a struct FileType
which will hold our Filetype information for
now.
@@ -1,3 +1,4 @@
|
|
1
|
+
use crate::FileType;
|
1
2
|
use crate::Position;
|
2
3
|
use crate::Row;
|
3
4
|
use crate::SearchDirection;
|
@@ -9,6 +10,7 @@ pub struct Document {
|
|
9
10
|
rows: Vec<Row>,
|
10
11
|
pub file_name: Option<String>,
|
11
12
|
dirty: bool,
|
13
|
+
file_type: FileType,
|
12
14
|
}
|
13
15
|
|
14
16
|
impl Document {
|
@@ -24,6 +26,7 @@ impl Document {
|
|
24
26
|
rows,
|
25
27
|
file_name: Some(filename.to_string()),
|
26
28
|
dirty: false,
|
29
|
+
file_type: FileType::default(),
|
27
30
|
})
|
28
31
|
}
|
29
32
|
pub fn row(&self, index: usize) -> Option<&Row> {
|
@@ -0,0 +1,18 @@
|
|
1
|
+
pub struct FileType {
|
2
|
+
name: String,
|
3
|
+
hl_opts: HighlightingOptions,
|
4
|
+
}
|
5
|
+
|
6
|
+
#[derive(Default)]
|
7
|
+
pub struct HighlightingOptions {
|
8
|
+
pub numbers: bool,
|
9
|
+
}
|
10
|
+
|
11
|
+
impl Default for FileType {
|
12
|
+
fn default() -> Self {
|
13
|
+
Self {
|
14
|
+
name: String::from("No filetype"),
|
15
|
+
hl_opts: HighlightingOptions::default(),
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
@@ -9,6 +9,7 @@
|
|
9
9
|
)]
|
10
10
|
mod document;
|
11
11
|
mod editor;
|
12
|
+
mod filetype;
|
12
13
|
mod highlighting;
|
13
14
|
mod row;
|
14
15
|
mod terminal;
|
@@ -16,6 +17,7 @@ pub use document::Document;
|
|
16
17
|
use editor::Editor;
|
17
18
|
pub use editor::Position;
|
18
19
|
pub use editor::SearchDirection;
|
20
|
+
pub use filetype::FileType;
|
19
21
|
pub use row::Row;
|
20
22
|
pub use terminal::Terminal;
|
21
23
|
|
HighlightingOptions
will hold a couple of booleans which determine whether or
not a certain type should be highlighted. Since it’s closely related to the file
type, we keep both in the same file. For now, we only add numbers
to determine
whether or not numbers should be highlighted. We use #[derive(Default)]
for
this struct so that HighlightOptions::default
returns HighlightOptions
initialized with default values. Since HighlightOptions
will only contain
bool
s, and the default for bools is false
, this suits us well and means that
when we add a highlighting option, we only need to change it where we need it,
for everything else it will just be unused.
We implement default
for FileType
, this time, we set the string to "No
filetype"
and a default HighlightingOptions
object. For now, we only use the
default file type, even when opening files. Let’s get the filetype displayed in
the editor.
@@ -29,6 +29,9 @@ impl Document {
|
|
29
29
|
file_type: FileType::default(),
|
30
30
|
})
|
31
31
|
}
|
32
|
+
pub fn file_type(&self) -> String {
|
33
|
+
self.file_type.name()
|
34
|
+
}
|
32
35
|
pub fn row(&self, index: usize) -> Option<&Row> {
|
33
36
|
self.rows.get(index)
|
34
37
|
}
|
@@ -343,7 +343,8 @@ impl Editor {
|
|
343
343
|
);
|
344
344
|
|
345
345
|
let line_indicator = format!(
|
346
|
-
"{}/{}",
|
346
|
+
"{} | {}/{}",
|
347
|
+
self.document.file_type(),
|
347
348
|
self.cursor_position.y.saturating_add(1),
|
348
349
|
self.document.len()
|
349
350
|
);
|
@@ -16,3 +16,9 @@ impl Default for FileType {
|
|
16
16
|
}
|
17
17
|
}
|
18
18
|
}
|
19
|
+
|
20
|
+
impl FileType {
|
21
|
+
pub fn name(&self) -> String {
|
22
|
+
self.name.clone()
|
23
|
+
}
|
24
|
+
}
|
We have decided against making the name property of FileType
directly editable
by Document
, and we have further decided to hide away the fact that FileType
is a struct
from Editor
. As far as the editor is concerned, FileType
is
simply a string.
Now we need a way to detect the file type and set the correct
Highlighting_Options
.
@@ -26,7 +26,7 @@ impl Document {
|
|
26
26
|
rows,
|
27
27
|
file_name: Some(filename.to_string()),
|
28
28
|
dirty: false,
|
29
|
-
file_type: FileType::
|
29
|
+
file_type: FileType::from(filename),
|
30
30
|
})
|
31
31
|
}
|
32
32
|
pub fn file_type(&self) -> String {
|
@@ -103,6 +103,7 @@ impl Document {
|
|
103
103
|
file.write_all(row.as_bytes())?;
|
104
104
|
file.write_all(b"\n")?;
|
105
105
|
}
|
106
|
+
self.file_type = FileType::from(file_name);
|
106
107
|
self.dirty = false;
|
107
108
|
}
|
108
109
|
Ok(())
|
@@ -21,4 +21,13 @@ impl FileType {
|
|
21
21
|
pub fn name(&self) -> String {
|
22
22
|
self.name.clone()
|
23
23
|
}
|
24
|
+
pub fn from(file_name: &str) -> Self {
|
25
|
+
if file_name.ends_with(".rs") {
|
26
|
+
return Self {
|
27
|
+
name: String::from("Rust"),
|
28
|
+
hl_opts: HighlightingOptions { numbers: true },
|
29
|
+
};
|
30
|
+
}
|
31
|
+
Self::default()
|
32
|
+
}
|
24
33
|
}
|
We add from
to FileType
to determine the file type from its name. If we
don’t know the type, we simply return the default
value. We set the file type
on open
and on save
. You are now able to open a file, verify it displays the
correct file type, and confirm that the file type changes when you change the
file ending on save. Very satisfying!
Now, let’s actually highlight the files. For that, we need to actually do
something with the HighlightingOptions
.
@@ -16,17 +16,18 @@ pub struct Document {
|
|
16
16
|
impl Document {
|
17
17
|
pub fn open(filename: &str) -> Result<Self, std::io::Error> {
|
18
18
|
let contents = fs::read_to_string(filename)?;
|
19
|
+
let file_type = FileType::from(filename);
|
19
20
|
let mut rows = Vec::new();
|
20
21
|
for value in contents.lines() {
|
21
22
|
let mut row = Row::from(value);
|
22
|
-
row.highlight(None);
|
23
|
+
row.highlight(file_type.highlighting_options(), None);
|
23
24
|
rows.push(row);
|
24
25
|
}
|
25
26
|
Ok(Self {
|
26
27
|
rows,
|
27
28
|
file_name: Some(filename.to_string()),
|
28
29
|
dirty: false,
|
29
|
-
file_type
|
30
|
+
file_type,
|
30
31
|
})
|
31
32
|
}
|
32
33
|
pub fn file_type(&self) -> String {
|
@@ -52,8 +53,8 @@ impl Document {
|
|
52
53
|
#[allow(clippy::indexing_slicing)]
|
53
54
|
let current_row = &mut self.rows[at.y];
|
54
55
|
let mut new_row = current_row.split(at.x);
|
55
|
-
current_row.highlight(None);
|
56
|
-
new_row.highlight(None);
|
56
|
+
current_row.highlight(self.file_type.highlighting_options(), None);
|
57
|
+
new_row.highlight(self.file_type.highlighting_options(), None);
|
57
58
|
#[allow(clippy::integer_arithmetic)]
|
58
59
|
self.rows.insert(at.y + 1, new_row);
|
59
60
|
}
|
@@ -69,13 +70,13 @@ impl Document {
|
|
69
70
|
if at.y == self.rows.len() {
|
70
71
|
let mut row = Row::default();
|
71
72
|
row.insert(0, c);
|
72
|
-
row.highlight(None);
|
73
|
+
row.highlight(self.file_type.highlighting_options(), None);
|
73
74
|
self.rows.push(row);
|
74
75
|
} else {
|
75
76
|
#[allow(clippy::indexing_slicing)]
|
76
77
|
let row = &mut self.rows[at.y];
|
77
78
|
row.insert(at.x, c);
|
78
|
-
row.highlight(None);
|
79
|
+
row.highlight(self.file_type.highlighting_options(), None);
|
79
80
|
}
|
80
81
|
}
|
81
82
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
@@ -89,11 +90,11 @@ impl Document {
|
|
89
90
|
let next_row = self.rows.remove(at.y + 1);
|
90
91
|
let row = &mut self.rows[at.y];
|
91
92
|
row.append(&next_row);
|
92
|
-
row.highlight(None);
|
93
|
+
row.highlight(self.file_type.highlighting_options(), None);
|
93
94
|
} else {
|
94
95
|
let row = &mut self.rows[at.y];
|
95
96
|
row.delete(at.x);
|
96
|
-
row.highlight(None);
|
97
|
+
row.highlight(self.file_type.highlighting_options(), None);
|
97
98
|
}
|
98
99
|
}
|
99
100
|
pub fn save(&mut self) -> Result<(), Error> {
|
@@ -149,7 +150,7 @@ impl Document {
|
|
149
150
|
}
|
150
151
|
pub fn highlight(&mut self, word: Option<&str>) {
|
151
152
|
for row in &mut self.rows {
|
152
|
-
row.highlight(word);
|
153
|
+
row.highlight(self.file_type.highlighting_options(), word);
|
153
154
|
}
|
154
155
|
}
|
155
156
|
}
|
@@ -3,7 +3,7 @@ pub struct FileType {
|
|
3
3
|
hl_opts: HighlightingOptions,
|
4
4
|
}
|
5
5
|
|
6
|
-
#[derive(Default)]
|
6
|
+
#[derive(Default, Copy, Clone)]
|
7
7
|
pub struct HighlightingOptions {
|
8
8
|
pub numbers: bool,
|
9
9
|
}
|
@@ -21,6 +21,9 @@ impl FileType {
|
|
21
21
|
pub fn name(&self) -> String {
|
22
22
|
self.name.clone()
|
23
23
|
}
|
24
|
+
pub fn highlighting_options(&self) -> HighlightingOptions {
|
25
|
+
self.hl_opts
|
26
|
+
}
|
24
27
|
pub fn from(file_name: &str) -> Self {
|
25
28
|
if file_name.ends_with(".rs") {
|
26
29
|
return Self {
|
@@ -18,6 +18,7 @@ use editor::Editor;
|
|
18
18
|
pub use editor::Position;
|
19
19
|
pub use editor::SearchDirection;
|
20
20
|
pub use filetype::FileType;
|
21
|
+
pub use filetype::HighlightingOptions;
|
21
22
|
pub use row::Row;
|
22
23
|
pub use terminal::Terminal;
|
23
24
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
use crate::highlighting;
|
2
|
+
use crate::HighlightingOptions;
|
2
3
|
use crate::SearchDirection;
|
3
4
|
use std::cmp;
|
4
5
|
use termion::color;
|
@@ -163,7 +164,7 @@ impl Row {
|
|
163
164
|
}
|
164
165
|
None
|
165
166
|
}
|
166
|
-
pub fn highlight(&mut self, word: Option<&str>) {
|
167
|
+
pub fn highlight(&mut self, opts: HighlightingOptions, word: Option<&str>) {
|
167
168
|
let mut highlighting = Vec::new();
|
168
169
|
let chars: Vec<char> = self.string.chars().collect();
|
169
170
|
let mut matches = Vec::new();
|
@@ -200,11 +201,15 @@ impl Row {
|
|
200
201
|
} else {
|
201
202
|
&highlighting::Type::None
|
202
203
|
};
|
203
|
-
if
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
204
|
+
if opts.numbers {
|
205
|
+
if (c.is_ascii_digit()
|
206
|
+
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number))
|
207
|
+
|| (c == &'.' && previous_highlight == &highlighting::Type::Number)
|
208
|
+
{
|
209
|
+
highlighting.push(highlighting::Type::Number);
|
210
|
+
} else {
|
211
|
+
highlighting.push(highlighting::Type::None);
|
212
|
+
}
|
208
213
|
} else {
|
209
214
|
highlighting.push(highlighting::Type::None);
|
210
215
|
}
|
The bulk of this change deals with passing around the highlighting options, so
that Row
has it available when its being highlighted. Then, within
highlight
, we simply wrap the highlighting for numbers in another if
statement which checks whether or not numbers
is enabled.
You can now see that Rust files are highlighted correctly, and non-rust files are not highlighted. But what is that? When you save a new file as a rust file, the highlighting starts acting all weird. Very unsatisfying!
While we’re at it, let’s also fix another minor nit-pick: We have taken great
effort to ensure no one can write the file type and the highlighting options,
but we keep the members of HighlightingOptions
open for anyone to edit.
@@ -100,11 +100,12 @@ impl Document {
|
|
100
100
|
pub fn save(&mut self) -> Result<(), Error> {
|
101
101
|
if let Some(file_name) = &self.file_name {
|
102
102
|
let mut file = fs::File::create(file_name)?;
|
103
|
-
|
103
|
+
self.file_type = FileType::from(file_name);
|
104
|
+
for row in &mut self.rows {
|
104
105
|
file.write_all(row.as_bytes())?;
|
105
106
|
file.write_all(b"\n")?;
|
107
|
+
row.highlight(self.file_type.highlighting_options(), None)
|
106
108
|
}
|
107
|
-
self.file_type = FileType::from(file_name);
|
108
109
|
self.dirty = false;
|
109
110
|
}
|
110
111
|
Ok(())
|
@@ -5,7 +5,7 @@ pub struct FileType {
|
|
5
5
|
|
6
6
|
#[derive(Default, Copy, Clone)]
|
7
7
|
pub struct HighlightingOptions {
|
8
|
-
|
8
|
+
numbers: bool,
|
9
9
|
}
|
10
10
|
|
11
11
|
impl Default for FileType {
|
@@ -34,3 +34,9 @@ impl FileType {
|
|
34
34
|
Self::default()
|
35
35
|
}
|
36
36
|
}
|
37
|
+
|
38
|
+
impl HighlightingOptions {
|
39
|
+
pub fn numbers(self) -> bool {
|
40
|
+
self.numbers
|
41
|
+
}
|
42
|
+
}
|
@@ -201,7 +201,7 @@ impl Row {
|
|
201
201
|
} else {
|
202
202
|
&highlighting::Type::None
|
203
203
|
};
|
204
|
-
if opts.numbers {
|
204
|
+
if opts.numbers() {
|
205
205
|
if (c.is_ascii_digit()
|
206
206
|
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number))
|
207
207
|
|| (c == &'.' && previous_highlight == &highlighting::Type::Number)
|
We are now re-highlighting each row as we save the file, and our number
property is now finally read-only.
A quick side note: If you’re eagle-eyed, you will have noticed that the function
numbers
accepts self
instead of &self
(Notice the missing &
). The
distinction is not important in the scope of this tutorial, and as our struct
grows, we will be reverting back to &
soon. In a nutshell, for small structs,
it’s more efficient to work directly with the value (without the &
) than with
the reference. Try adding the &
and check the clippy output in case you’re
interested.
Colorful strings
With all that out of the way, we can finally get to highlighting more things! Let’s start with highlighting strings.
@@ -6,6 +6,7 @@ pub struct FileType {
|
|
6
6
|
#[derive(Default, Copy, Clone)]
|
7
7
|
pub struct HighlightingOptions {
|
8
8
|
numbers: bool,
|
9
|
+
strings: bool,
|
9
10
|
}
|
10
11
|
|
11
12
|
impl Default for FileType {
|
@@ -28,7 +29,10 @@ impl FileType {
|
|
28
29
|
if file_name.ends_with(".rs") {
|
29
30
|
return Self {
|
30
31
|
name: String::from("Rust"),
|
31
|
-
hl_opts: HighlightingOptions {
|
32
|
+
hl_opts: HighlightingOptions {
|
33
|
+
numbers: true,
|
34
|
+
strings: true,
|
35
|
+
},
|
32
36
|
};
|
33
37
|
}
|
34
38
|
Self::default()
|
@@ -39,4 +43,7 @@ impl HighlightingOptions {
|
|
39
43
|
pub fn numbers(self) -> bool {
|
40
44
|
self.numbers
|
41
45
|
}
|
46
|
+
pub fn strings(self) -> bool {
|
47
|
+
self.strings
|
48
|
+
}
|
42
49
|
}
|
@@ -4,6 +4,7 @@ pub enum Type {
|
|
4
4
|
None,
|
5
5
|
Number,
|
6
6
|
Match,
|
7
|
+
String,
|
7
8
|
}
|
8
9
|
|
9
10
|
impl Type {
|
@@ -11,6 +12,7 @@ impl Type {
|
|
11
12
|
match self {
|
12
13
|
Type::Number => color::Rgb(220, 163, 163),
|
13
14
|
Type::Match => color::Rgb(38, 139, 210),
|
15
|
+
Type::String => color::Rgb(211, 54, 130),
|
14
16
|
_ => color::Rgb(255, 255, 255),
|
15
17
|
}
|
16
18
|
}
|
Now for the actual highlighting code. We will use an in_string
variable to
keep track of whether we are currently inside a string. If we are, then we’ll
keep highlighting the current character as a string until we hit the closing
quote.
@@ -182,6 +182,7 @@ impl Row {
|
|
182
182
|
}
|
183
183
|
}
|
184
184
|
let mut prev_is_separator = true;
|
185
|
+
let mut in_string = false;
|
185
186
|
let mut index = 0;
|
186
187
|
while let Some(c) = chars.get(index) {
|
187
188
|
if let Some(word) = word {
|
@@ -201,6 +202,25 @@ impl Row {
|
|
201
202
|
} else {
|
202
203
|
&highlighting::Type::None
|
203
204
|
};
|
205
|
+
if opts.strings() {
|
206
|
+
if in_string {
|
207
|
+
highlighting.push(highlighting::Type::String);
|
208
|
+
if *c == '"' {
|
209
|
+
in_string = false;
|
210
|
+
prev_is_separator = true;
|
211
|
+
} else {
|
212
|
+
prev_is_separator = false;
|
213
|
+
}
|
214
|
+
index += 1;
|
215
|
+
continue;
|
216
|
+
} else if prev_is_separator && *c == '"' {
|
217
|
+
highlighting.push(highlighting::Type::String);
|
218
|
+
in_string = true;
|
219
|
+
prev_is_separator = true;
|
220
|
+
index += 1;
|
221
|
+
continue;
|
222
|
+
}
|
223
|
+
}
|
204
224
|
if opts.numbers() {
|
205
225
|
if (c.is_ascii_digit()
|
206
226
|
&& (prev_is_separator || previous_highlight == &highlighting::Type::Number))
|
Let’sa go through this change from top to bottom: If in_string
is set, then we
know that the current character can be highlighted as a string. Then we check if
the current character is the closing quote, and if so, we reset in_string
to
false
. Then, since we highlighted the current character, we have to consume it
by incrementing index
and continue
ing out of the current loop iteration. We
also set prev_is_separator
to true
so that if we’re done highlighting the
string, the closing quote is considered a separator.
If we’re not currently in a string, then we have to check if we’re at the
beginning of one by checking for the starting quote. If we are, we set
in_string
to true
, highlight the quote and consume it.
We have also introduced a new concept here: The dereferencing operator,
*
.. The
point is that c
in this code is a reference to a character, and we check if it
is the desired character by comparing it with a reference to another character,
like c == &'"'
. Alternatively, we can dereference c and directly compare it
to the desired character. This makes the code a bit easier to read. Let’s apply
this in other parts as well.
@@ -223,8 +223,8 @@ impl Row {
|
|
223
223
|
}
|
224
224
|
if opts.numbers() {
|
225
225
|
if (c.is_ascii_digit()
|
226
|
-
&& (prev_is_separator || previous_highlight ==
|
227
|
-
|| (c ==
|
226
|
+
&& (prev_is_separator || *previous_highlight == highlighting::Type::Number))
|
227
|
+
|| (*c == '.' && *previous_highlight == highlighting::Type::Number)
|
228
228
|
{
|
229
229
|
highlighting.push(highlighting::Type::Number);
|
230
230
|
} else {
|
We should probably take escaped quotes into account when highlighting strings
and characters. If the sequence \"
occurs in a string, then the escaped quote
doesn’t close the string in the vast majority of languages.
@@ -205,6 +205,12 @@ impl Row {
|
|
205
205
|
if opts.strings() {
|
206
206
|
if in_string {
|
207
207
|
highlighting.push(highlighting::Type::String);
|
208
|
+
|
209
|
+
if *c == '\\' && index < self.len().saturating_sub(1) {
|
210
|
+
highlighting.push(highlighting::Type::String);
|
211
|
+
index += 2;
|
212
|
+
continue;
|
213
|
+
}
|
208
214
|
if *c == '"' {
|
209
215
|
in_string = false;
|
210
216
|
prev_is_separator = true;
|
If we’re in a string and the current character is a \
, and there is at least
one more character in that line that comes after the backslash, then we highligh
the character that comes after the backslash with HighlightingType::String
and
consume it. We increment index
by 2
to consume both characters at once.
Colorful characters
Now that strings work, let’s focus on characters next. We start with the basics first.
@@ -7,6 +7,7 @@ pub struct FileType {
|
|
7
7
|
pub struct HighlightingOptions {
|
8
8
|
numbers: bool,
|
9
9
|
strings: bool,
|
10
|
+
characters: bool,
|
10
11
|
}
|
11
12
|
|
12
13
|
impl Default for FileType {
|
@@ -32,6 +33,7 @@ impl FileType {
|
|
32
33
|
hl_opts: HighlightingOptions {
|
33
34
|
numbers: true,
|
34
35
|
strings: true,
|
36
|
+
characters: true,
|
35
37
|
},
|
36
38
|
};
|
37
39
|
}
|
@@ -46,4 +48,7 @@ impl HighlightingOptions {
|
|
46
48
|
pub fn strings(self) -> bool {
|
47
49
|
self.strings
|
48
50
|
}
|
51
|
+
pub fn characters(self) -> bool {
|
52
|
+
self.characters
|
53
|
+
}
|
49
54
|
}
|
@@ -5,6 +5,7 @@ pub enum Type {
|
|
5
5
|
Number,
|
6
6
|
Match,
|
7
7
|
String,
|
8
|
+
Character,
|
8
9
|
}
|
9
10
|
|
10
11
|
impl Type {
|
@@ -13,6 +14,7 @@ impl Type {
|
|
13
14
|
Type::Number => color::Rgb(220, 163, 163),
|
14
15
|
Type::Match => color::Rgb(38, 139, 210),
|
15
16
|
Type::String => color::Rgb(211, 54, 130),
|
17
|
+
Type::Character => color::Rgb(108, 113, 196),
|
16
18
|
_ => color::Rgb(255, 255, 255),
|
17
19
|
}
|
18
20
|
}
|
You might want to do character highlighting the same way as we highlight
strings. That would create two problems, though: First, we would over-eagerly
highlight nonsense like 'this is definitely no character'
. Second, it would
not work with Lifetimes:
A lifetime can be indicated with a single '
. Our highlighting would not take
this into account and highlight everything after the opening '
as a character.
When we highlight strings, we are also not looking for a closing character and simply end the highlighting of a string when the line ends. That’s fine, since an unclosed string is probably a typo anyways, and that typo is easier to spot if string-highlighting goes from the opening quotes to the end than with no highlighting at all.
Let’s see how we can implement character highlighting:
@@ -202,6 +202,28 @@ impl Row {
|
|
202
202
|
} else {
|
203
203
|
&highlighting::Type::None
|
204
204
|
};
|
205
|
+
if opts.characters() && !in_string && *c == '\'' {
|
206
|
+
prev_is_separator = true;
|
207
|
+
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
208
|
+
let closing_index = if *next_char == '\\' {
|
209
|
+
index.saturating_add(3)
|
210
|
+
} else {
|
211
|
+
index.saturating_add(2)
|
212
|
+
};
|
213
|
+
if let Some(closing_char) = chars.get(closing_index) {
|
214
|
+
if *closing_char == '\'' {
|
215
|
+
for _ in 0..=closing_index.saturating_sub(index) {
|
216
|
+
highlighting.push(highlighting::Type::Character);
|
217
|
+
index += 1;
|
218
|
+
}
|
219
|
+
continue;
|
220
|
+
}
|
221
|
+
}
|
222
|
+
};
|
223
|
+
highlighting.push(highlighting::Type::None);
|
224
|
+
index += 1;
|
225
|
+
continue;
|
226
|
+
}
|
205
227
|
if opts.strings() {
|
206
228
|
if in_string {
|
207
229
|
highlighting.push(highlighting::Type::String);
|
We are handling character highlighting before string highlighting, but only if we are not currently within a string. That way, we can make sure that on the one hand side, string opening quotes are not handled within a character, and on the other hand side, character opening quotes are not handled within a string.
If we are on a character opening quote, our plan is to handle three different cases:
- We are on a
'
and the next character is anything other than\
. Then we look at the character after the next. If it’s a'
, we highlight all three characters. In other words, we are handling'*'
here. - We are on a
'
and the next character is a\
. If it is, we look one character further. If it’s a'
, we highlight all four characters. In other words, we are handling escaped characters,\*'
, here. - In all other cases, we are not highlighting the current
'
as a character and advance the index by one, consuming the'
.
Looking at the code, we start by setting prev_is_separator
to true
: In all
three cases, when we are done, we want the last consumed character to be treated
as a separator, as it will always be a '
.
Next, we are looking at the next character to determine where we expect the '
to match the opening quote. If we are looking at a \
as the next character, we
expect the closing character being the 3rd character after the current
character. Otherwise, we expect it to be the 2nd.
We are then looking at the character we expect to be the closing quote. If it is
the closing quote, we are consuming all characters up and including the closing
quote. (The =
in the for..in
loop means that we are going to include the
last index of the provided range)
If we did not find the closing quote where we expect it to be, we are simply
highlighting the current '
as None
and consume it.
Colorful single-line comments
Next, let’s highlight single-line comments. (We’ll leave multi-line comments until the end, because they’re complicated).
@@ -8,6 +8,7 @@ pub struct HighlightingOptions {
|
|
8
8
|
numbers: bool,
|
9
9
|
strings: bool,
|
10
10
|
characters: bool,
|
11
|
+
comments: bool,
|
11
12
|
}
|
12
13
|
|
13
14
|
impl Default for FileType {
|
@@ -34,6 +35,7 @@ impl FileType {
|
|
34
35
|
numbers: true,
|
35
36
|
strings: true,
|
36
37
|
characters: true,
|
38
|
+
comments: true,
|
37
39
|
},
|
38
40
|
};
|
39
41
|
}
|
@@ -51,4 +53,7 @@ impl HighlightingOptions {
|
|
51
53
|
pub fn characters(self) -> bool {
|
52
54
|
self.characters
|
53
55
|
}
|
56
|
+
pub fn comments(self) -> bool {
|
57
|
+
self.comments
|
58
|
+
}
|
54
59
|
}
|
@@ -6,6 +6,7 @@ pub enum Type {
|
|
6
6
|
Match,
|
7
7
|
String,
|
8
8
|
Character,
|
9
|
+
Comment,
|
9
10
|
}
|
10
11
|
|
11
12
|
impl Type {
|
@@ -15,6 +16,7 @@ impl Type {
|
|
15
16
|
Type::Match => color::Rgb(38, 139, 210),
|
16
17
|
Type::String => color::Rgb(211, 54, 130),
|
17
18
|
Type::Character => color::Rgb(108, 113, 196),
|
19
|
+
Type::Comment => color::Rgb(133, 153, 0),
|
18
20
|
_ => color::Rgb(255, 255, 255),
|
19
21
|
}
|
20
22
|
}
|
Single line comments are easy to detect: If we encounter a /
outside of a
string, all we need to do is check if it is followed by another /
. If so,
treat the whole rest of the line as a comment.
@@ -249,6 +249,17 @@ impl Row {
|
|
249
249
|
continue;
|
250
250
|
}
|
251
251
|
}
|
252
|
+
|
253
|
+
if opts.comments() && *c == '/' {
|
254
|
+
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
255
|
+
if *next_char == '/' {
|
256
|
+
for _ in index..chars.len() {
|
257
|
+
highlighting.push(highlighting::Type::Comment);
|
258
|
+
}
|
259
|
+
break;
|
260
|
+
}
|
261
|
+
};
|
262
|
+
}
|
252
263
|
if opts.numbers() {
|
253
264
|
if (c.is_ascii_digit()
|
254
265
|
&& (prev_is_separator || *previous_highlight == highlighting::Type::Number))
|
You should now be able to confirm that highlighting single line comments works. Before we move to other things, however, we need to improve our code.
Improve code quality
If you are following along using Clippy, you might have noticed that Clippy now
highlights the entirety of our highlight function. The grievance that it wants
to point out is that our function is getting too long. Adding to this function
is easy as we go along, but if you take a step back and try to take the whole
thing in, then you will realize just how difficult it is to follow along. In one
of the early chapters of the tutorial, this was the precise reason why we
started taking stuff out of main
: To maintain readability. We are going to do
the same thing now.
Our strategy will be to split our big highlight
function into separate
independent functions. Each function can either return false
, meaning that
they did not highlight the current character, or true
, indicating that they
handled the character. We are also going to allow these functions to modify the
pointer to the current character, to allow each function to consume more than
one character at a time.
@@ -164,118 +164,148 @@ impl Row {
|
|
164
164
|
}
|
165
165
|
None
|
166
166
|
}
|
167
|
-
pub fn highlight(&mut self, opts: HighlightingOptions, word: Option<&str>) {
|
168
|
-
let mut highlighting = Vec::new();
|
169
|
-
let chars: Vec<char> = self.string.chars().collect();
|
170
|
-
let mut matches = Vec::new();
|
171
|
-
let mut search_index = 0;
|
172
167
|
|
168
|
+
fn highlight_match(&mut self, word: Option<&str>) {
|
173
169
|
if let Some(word) = word {
|
174
|
-
|
175
|
-
|
170
|
+
if word.is_empty() {
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
let mut index = 0;
|
174
|
+
while let Some(search_match) = self.find(word, index, SearchDirection::Forward) {
|
176
175
|
if let Some(next_index) = search_match.checked_add(word[..].graphemes(true).count())
|
177
176
|
{
|
178
|
-
|
177
|
+
#[allow(clippy::indexing_slicing)]
|
178
|
+
for i in index.saturating_add(search_match)..next_index {
|
179
|
+
self.highlighting[i] = highlighting::Type::Match;
|
180
|
+
}
|
181
|
+
index = next_index;
|
179
182
|
} else {
|
180
183
|
break;
|
181
184
|
}
|
182
185
|
}
|
183
186
|
}
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
187
|
+
}
|
188
|
+
|
189
|
+
fn highlight_char(
|
190
|
+
&mut self,
|
191
|
+
index: &mut usize,
|
192
|
+
opts: HighlightingOptions,
|
193
|
+
c: char,
|
194
|
+
chars: &[char],
|
195
|
+
) -> bool {
|
196
|
+
if opts.characters() && c == '\'' {
|
197
|
+
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
198
|
+
let closing_index = if *next_char == '\\' {
|
199
|
+
index.saturating_add(3)
|
200
|
+
} else {
|
201
|
+
index.saturating_add(2)
|
202
|
+
};
|
203
|
+
if let Some(closing_char) = chars.get(closing_index) {
|
204
|
+
if *closing_char == '\'' {
|
205
|
+
for _ in 0..=closing_index.saturating_sub(*index) {
|
206
|
+
self.highlighting.push(highlighting::Type::Character);
|
207
|
+
*index += 1;
|
205
|
-
if opts.characters() && !in_string && *c == '\'' {
|
206
|
-
prev_is_separator = true;
|
207
|
-
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
208
|
-
let closing_index = if *next_char == '\\' {
|
209
|
-
index.saturating_add(3)
|
210
|
-
} else {
|
211
|
-
index.saturating_add(2)
|
212
|
-
};
|
213
|
-
if let Some(closing_char) = chars.get(closing_index) {
|
214
|
-
if *closing_char == '\'' {
|
215
|
-
for _ in 0..=closing_index.saturating_sub(index) {
|
216
|
-
highlighting.push(highlighting::Type::Character);
|
217
|
-
index += 1;
|
218
|
-
}
|
219
|
-
continue;
|
220
208
|
}
|
209
|
+
return true;
|
221
210
|
}
|
222
|
-
}
|
211
|
+
}
|
223
|
-
highlighting.push(highlighting::Type::None);
|
224
|
-
index += 1;
|
225
|
-
continue;
|
226
212
|
}
|
227
|
-
|
228
|
-
|
229
|
-
|
213
|
+
}
|
214
|
+
false
|
215
|
+
}
|
230
216
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
217
|
+
fn highlight_comment(
|
218
|
+
&mut self,
|
219
|
+
index: &mut usize,
|
220
|
+
opts: HighlightingOptions,
|
221
|
+
c: char,
|
222
|
+
chars: &[char],
|
223
|
+
) -> bool {
|
224
|
+
if opts.comments() && c == '/' && *index < chars.len() {
|
225
|
+
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
226
|
+
if *next_char == '/' {
|
227
|
+
for _ in *index..chars.len() {
|
228
|
+
self.highlighting.push(highlighting::Type::Comment);
|
229
|
+
*index += 1;
|
235
230
|
}
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
231
|
+
return true;
|
232
|
+
}
|
233
|
+
};
|
234
|
+
}
|
235
|
+
false
|
236
|
+
}
|
237
|
+
|
238
|
+
fn highlight_string(
|
239
|
+
&mut self,
|
240
|
+
index: &mut usize,
|
241
|
+
opts: HighlightingOptions,
|
242
|
+
c: char,
|
243
|
+
chars: &[char],
|
244
|
+
) -> bool {
|
245
|
+
if opts.strings() && c == '"' {
|
246
|
+
loop {
|
247
|
+
self.highlighting.push(highlighting::Type::String);
|
248
|
+
*index += 1;
|
249
|
+
if let Some(next_char) = chars.get(*index) {
|
250
|
+
if *next_char == '"' {
|
251
|
+
break;
|
241
252
|
}
|
242
|
-
|
243
|
-
|
253
|
+
} else {
|
254
|
+
break;
|
244
|
-
} else if prev_is_separator && *c == '"' {
|
245
|
-
highlighting.push(highlighting::Type::String);
|
246
|
-
in_string = true;
|
247
|
-
prev_is_separator = true;
|
248
|
-
index += 1;
|
249
|
-
continue;
|
250
255
|
}
|
251
256
|
}
|
257
|
+
self.highlighting.push(highlighting::Type::String);
|
258
|
+
*index += 1;
|
259
|
+
return true;
|
260
|
+
}
|
261
|
+
false
|
262
|
+
}
|
252
263
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
264
|
+
fn highlight_number(
|
265
|
+
&mut self,
|
266
|
+
index: &mut usize,
|
267
|
+
opts: HighlightingOptions,
|
268
|
+
c: char,
|
269
|
+
chars: &[char],
|
270
|
+
) -> bool {
|
271
|
+
if opts.numbers() && c.is_ascii_digit() {
|
272
|
+
if *index > 0 {
|
273
|
+
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
274
|
+
let prev_char = chars[*index - 1];
|
275
|
+
if !prev_char.is_ascii_punctuation() && !prev_char.is_ascii_whitespace() {
|
276
|
+
return false;
|
277
|
+
}
|
278
|
+
}
|
279
|
+
loop {
|
280
|
+
self.highlighting.push(highlighting::Type::Number);
|
281
|
+
*index += 1;
|
282
|
+
if let Some(next_char) = chars.get(*index) {
|
283
|
+
if *next_char != '.' && !next_char.is_ascii_digit() {
|
259
284
|
break;
|
260
285
|
}
|
261
|
-
};
|
262
|
-
}
|
263
|
-
if opts.numbers() {
|
264
|
-
if (c.is_ascii_digit()
|
265
|
-
&& (prev_is_separator || *previous_highlight == highlighting::Type::Number))
|
266
|
-
|| (*c == '.' && *previous_highlight == highlighting::Type::Number)
|
267
|
-
{
|
268
|
-
highlighting.push(highlighting::Type::Number);
|
269
286
|
} else {
|
270
|
-
|
287
|
+
break;
|
271
288
|
}
|
272
|
-
} else {
|
273
|
-
highlighting.push(highlighting::Type::None);
|
274
289
|
}
|
275
|
-
|
290
|
+
return true;
|
291
|
+
}
|
292
|
+
false
|
293
|
+
}
|
294
|
+
pub fn highlight(&mut self, opts: HighlightingOptions, word: Option<&str>) {
|
295
|
+
self.highlighting = Vec::new();
|
296
|
+
let chars: Vec<char> = self.string.chars().collect();
|
297
|
+
let mut index = 0;
|
298
|
+
while let Some(c) = chars.get(index) {
|
299
|
+
if self.highlight_char(&mut index, opts, *c, &chars)
|
300
|
+
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
301
|
+
|| self.highlight_string(&mut index, opts, *c, &chars)
|
302
|
+
|| self.highlight_number(&mut index, opts, *c, &chars)
|
303
|
+
{
|
304
|
+
continue;
|
305
|
+
}
|
306
|
+
self.highlighting.push(highlighting::Type::None);
|
276
307
|
index += 1;
|
277
308
|
}
|
278
|
-
|
309
|
+
self.highlight_match(word);
|
279
|
-
self.highlighting = highlighting;
|
280
310
|
}
|
281
311
|
}
|
If you have trouble following this diff alone, don’t forget that the direct link below the diff lets you see the full file after the change.
Let’s go through this change by looking at highlight
first. That is the
function we wanted to simplify, and it has gotten so much smaller. We have
removed any highlighting logic except for the highlighting of None
, in case no
other highlighting has matched. By the way, if-statements are evaluated from
left to right, so, for instance, if highlight_comment
returns true
, none of
the other highlight functions is being called.
We will now investigate all the changes to all five highlighters we have written so far.
highlight_char
and highlight_comment
are largely unchanged, they have been
moved to separate functions and changed to return true
in case they do some
highlighting, and false
otherwise.
highlight_string
has changed quite a lot. Instead of having a variable which
keeps track of whether or not we are in a string, we are now checking if we are
at an opening quote. If so, we consume the remainder of the row until we meet
the end of it or a closing quote and highlight it as a string.
highlight_number
, with which we started the whole highlighting process, has
also changed slightly. If invoked, it checks if the previous character was a
separator (instead of having a variable keeping track of it). If so, the
highlighting is done now the same as in other functions: We consume the whole
number, including the dot, in one go before returning from this function.
We have now placed highlight_match
below the loop and have changed the logic a
bit. Previously, we were storing the indices of the matches to be used later
during highlighting. Now, we are updating self.highlighting
directly from
within highlight_match
. We need to handle this case separately as we want our
match not to interfere with other highlights. For instance, if we search for
con
, we want the first three letters of const
to be highlighted as a search
result, and the remaining two letters as a keyword.
Update: We now have a bug in our highlighting system. Can you spot it? Try to find different test cases and see if the highlighting still works as you would expect. Here is the solution.
Side note on refactoring
We have now done quite a big refactoring, and I want to say a word or two about the refactoring process itself. Many tutorials present you with a polished final solution which you can happily implement. But that is not how code evolves in practice. How we did it in this tutorial is much more realistic: You start with a simple problem, in this case, highlighting a single digit, and then you expand it over time. As the features grow, your code grows, too, and the assumptions you made at the beginning on what the code should look like will soon be outdated or plain wrong.
Being able to judge when to refactor is a skill that needs training, it doesn’t come by itself. Luckily, there are tools to help you build that skill. One is clippy. We could have ignored or silenced this warning, but ultimately, clippy was right in pointing out that our code was getting too complex! A refactoring was in order, if not overdue.
Is the code now perfect? Definitely not. There are at least the following two things wrong with it:
- The loop in
highlight
implicitly relies on the highlight functions to advanceindex
. If any of these functions returnstrue
, but does not modifyindex
, we run into an infinite loop. This is not obvious in the code and therefore not ideal. - The highlighting will not work around the borders of
usize
. We are pushing things into thehighlighting
array without checking if it is safe, and we are advancingindex
in many cases without any kind of check. It is not easy to see and understand how our code will behave in this case. Will it crash? Will it enter an infinite loop?
We are not going to solve these issues in this tutorial, but I invite you to take a stab at them, they are not terribly hard to solve.
If you are now disappointed that even after our refactoring our code still has flaws, then consider this: These flaws have been in the code before, but now they are much, much easier to see. Also, we wouldn’t have detected them if we hadn’t done the refactoring in the first place.
In a real world scenario, I would consider the weakness around large files as not relevant - we saw earlier why we would run in to all kinds of other problems first before these flaws would really become important, at least on modern systems.
I would also either refactor or at least document the other issue, to make sure that everyone who extends the highlighting in the future (could be my own future me) knows what to do.
Colorful keywords
Now let’s turn to highlighting keywords. We’re going to allow languages to specify two types of keywords that will be highlighted in different colors. (In Rust, we’ll highlight actual keywords in one color and common type names in the other color).
Let’s add two Vector
s to the HighlightingOptions
, one for primary, one for
secondary keywords.
@@ -20,7 +20,7 @@ impl Document {
|
|
20
20
|
let mut rows = Vec::new();
|
21
21
|
for value in contents.lines() {
|
22
22
|
let mut row = Row::from(value);
|
23
|
-
row.highlight(file_type.highlighting_options(), None);
|
23
|
+
row.highlight(&file_type.highlighting_options(), None);
|
24
24
|
rows.push(row);
|
25
25
|
}
|
26
26
|
Ok(Self {
|
@@ -53,8 +53,8 @@ impl Document {
|
|
53
53
|
#[allow(clippy::indexing_slicing)]
|
54
54
|
let current_row = &mut self.rows[at.y];
|
55
55
|
let mut new_row = current_row.split(at.x);
|
56
|
-
current_row.highlight(self.file_type.highlighting_options(), None);
|
57
|
-
new_row.highlight(self.file_type.highlighting_options(), None);
|
56
|
+
current_row.highlight(&self.file_type.highlighting_options(), None);
|
57
|
+
new_row.highlight(&self.file_type.highlighting_options(), None);
|
58
58
|
#[allow(clippy::integer_arithmetic)]
|
59
59
|
self.rows.insert(at.y + 1, new_row);
|
60
60
|
}
|
@@ -70,13 +70,13 @@ impl Document {
|
|
70
70
|
if at.y == self.rows.len() {
|
71
71
|
let mut row = Row::default();
|
72
72
|
row.insert(0, c);
|
73
|
-
row.highlight(self.file_type.highlighting_options(), None);
|
73
|
+
row.highlight(&self.file_type.highlighting_options(), None);
|
74
74
|
self.rows.push(row);
|
75
75
|
} else {
|
76
76
|
#[allow(clippy::indexing_slicing)]
|
77
77
|
let row = &mut self.rows[at.y];
|
78
78
|
row.insert(at.x, c);
|
79
|
-
row.highlight(self.file_type.highlighting_options(), None);
|
79
|
+
row.highlight(&self.file_type.highlighting_options(), None);
|
80
80
|
}
|
81
81
|
}
|
82
82
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
@@ -90,11 +90,11 @@ impl Document {
|
|
90
90
|
let next_row = self.rows.remove(at.y + 1);
|
91
91
|
let row = &mut self.rows[at.y];
|
92
92
|
row.append(&next_row);
|
93
|
-
row.highlight(self.file_type.highlighting_options(), None);
|
93
|
+
row.highlight(&self.file_type.highlighting_options(), None);
|
94
94
|
} else {
|
95
95
|
let row = &mut self.rows[at.y];
|
96
96
|
row.delete(at.x);
|
97
|
-
row.highlight(self.file_type.highlighting_options(), None);
|
97
|
+
row.highlight(&self.file_type.highlighting_options(), None);
|
98
98
|
}
|
99
99
|
}
|
100
100
|
pub fn save(&mut self) -> Result<(), Error> {
|
@@ -104,7 +104,7 @@ impl Document {
|
|
104
104
|
for row in &mut self.rows {
|
105
105
|
file.write_all(row.as_bytes())?;
|
106
106
|
file.write_all(b"\n")?;
|
107
|
-
row.highlight(self.file_type.highlighting_options(), None)
|
107
|
+
row.highlight(&self.file_type.highlighting_options(), None)
|
108
108
|
}
|
109
109
|
self.dirty = false;
|
110
110
|
}
|
@@ -151,7 +151,7 @@ impl Document {
|
|
151
151
|
}
|
152
152
|
pub fn highlight(&mut self, word: Option<&str>) {
|
153
153
|
for row in &mut self.rows {
|
154
|
-
row.highlight(self.file_type.highlighting_options(), word);
|
154
|
+
row.highlight(&self.file_type.highlighting_options(), word);
|
155
155
|
}
|
156
156
|
}
|
157
157
|
}
|
@@ -3,12 +3,14 @@ pub struct FileType {
|
|
3
3
|
hl_opts: HighlightingOptions,
|
4
4
|
}
|
5
5
|
|
6
|
-
#[derive(Default
|
6
|
+
#[derive(Default)]
|
7
7
|
pub struct HighlightingOptions {
|
8
8
|
numbers: bool,
|
9
9
|
strings: bool,
|
10
10
|
characters: bool,
|
11
11
|
comments: bool,
|
12
|
+
primary_keywords: Vec<String>,
|
13
|
+
secondary_keywords: Vec<String>,
|
12
14
|
}
|
13
15
|
|
14
16
|
impl Default for FileType {
|
@@ -24,8 +26,8 @@ impl FileType {
|
|
24
26
|
pub fn name(&self) -> String {
|
25
27
|
self.name.clone()
|
26
28
|
}
|
27
|
-
pub fn highlighting_options(&self) -> HighlightingOptions {
|
28
|
-
self.hl_opts
|
29
|
+
pub fn highlighting_options(&self) -> &HighlightingOptions {
|
30
|
+
&self.hl_opts
|
29
31
|
}
|
30
32
|
pub fn from(file_name: &str) -> Self {
|
31
33
|
if file_name.ends_with(".rs") {
|
@@ -36,6 +38,75 @@ impl FileType {
|
|
36
38
|
strings: true,
|
37
39
|
characters: true,
|
38
40
|
comments: true,
|
41
|
+
primary_keywords: vec![
|
42
|
+
"as".to_string(),
|
43
|
+
"break".to_string(),
|
44
|
+
"const".to_string(),
|
45
|
+
"continue".to_string(),
|
46
|
+
"crate".to_string(),
|
47
|
+
"else".to_string(),
|
48
|
+
"enum".to_string(),
|
49
|
+
"extern".to_string(),
|
50
|
+
"false".to_string(),
|
51
|
+
"fn".to_string(),
|
52
|
+
"for".to_string(),
|
53
|
+
"if".to_string(),
|
54
|
+
"impl".to_string(),
|
55
|
+
"in".to_string(),
|
56
|
+
"let".to_string(),
|
57
|
+
"loop".to_string(),
|
58
|
+
"match".to_string(),
|
59
|
+
"mod".to_string(),
|
60
|
+
"move".to_string(),
|
61
|
+
"mut".to_string(),
|
62
|
+
"pub".to_string(),
|
63
|
+
"ref".to_string(),
|
64
|
+
"return".to_string(),
|
65
|
+
"self".to_string(),
|
66
|
+
"Self".to_string(),
|
67
|
+
"static".to_string(),
|
68
|
+
"struct".to_string(),
|
69
|
+
"super".to_string(),
|
70
|
+
"trait".to_string(),
|
71
|
+
"true".to_string(),
|
72
|
+
"type".to_string(),
|
73
|
+
"unsafe".to_string(),
|
74
|
+
"use".to_string(),
|
75
|
+
"where".to_string(),
|
76
|
+
"while".to_string(),
|
77
|
+
"dyn".to_string(),
|
78
|
+
"abstract".to_string(),
|
79
|
+
"become".to_string(),
|
80
|
+
"box".to_string(),
|
81
|
+
"do".to_string(),
|
82
|
+
"final".to_string(),
|
83
|
+
"macro".to_string(),
|
84
|
+
"override".to_string(),
|
85
|
+
"priv".to_string(),
|
86
|
+
"typeof".to_string(),
|
87
|
+
"unsized".to_string(),
|
88
|
+
"virtual".to_string(),
|
89
|
+
"yield".to_string(),
|
90
|
+
"async".to_string(),
|
91
|
+
"await".to_string(),
|
92
|
+
"try".to_string(),
|
93
|
+
],
|
94
|
+
secondary_keywords: vec![
|
95
|
+
"bool".to_string(),
|
96
|
+
"char".to_string(),
|
97
|
+
"i8".to_string(),
|
98
|
+
"i16".to_string(),
|
99
|
+
"i32".to_string(),
|
100
|
+
"i64".to_string(),
|
101
|
+
"isize".to_string(),
|
102
|
+
"u8".to_string(),
|
103
|
+
"u16".to_string(),
|
104
|
+
"u32".to_string(),
|
105
|
+
"u64".to_string(),
|
106
|
+
"usize".to_string(),
|
107
|
+
"f32".to_string(),
|
108
|
+
"f64".to_string(),
|
109
|
+
],
|
39
110
|
},
|
40
111
|
};
|
41
112
|
}
|
@@ -44,16 +115,16 @@ impl FileType {
|
|
44
115
|
}
|
45
116
|
|
46
117
|
impl HighlightingOptions {
|
47
|
-
pub fn numbers(self) -> bool {
|
118
|
+
pub fn numbers(&self) -> bool {
|
48
119
|
self.numbers
|
49
120
|
}
|
50
|
-
pub fn strings(self) -> bool {
|
121
|
+
pub fn strings(&self) -> bool {
|
51
122
|
self.strings
|
52
123
|
}
|
53
|
-
pub fn characters(self) -> bool {
|
124
|
+
pub fn characters(&self) -> bool {
|
54
125
|
self.characters
|
55
126
|
}
|
56
|
-
pub fn comments(self) -> bool {
|
127
|
+
pub fn comments(&self) -> bool {
|
57
128
|
self.comments
|
58
129
|
}
|
59
130
|
}
|
@@ -7,6 +7,8 @@ pub enum Type {
|
|
7
7
|
String,
|
8
8
|
Character,
|
9
9
|
Comment,
|
10
|
+
PrimaryKeywords,
|
11
|
+
SecondaryKeywords,
|
10
12
|
}
|
11
13
|
|
12
14
|
impl Type {
|
@@ -17,6 +19,8 @@ impl Type {
|
|
17
19
|
Type::String => color::Rgb(211, 54, 130),
|
18
20
|
Type::Character => color::Rgb(108, 113, 196),
|
19
21
|
Type::Comment => color::Rgb(133, 153, 0),
|
22
|
+
Type::PrimaryKeywords => color::Rgb(181, 137, 0),
|
23
|
+
Type::SecondaryKeywords => color::Rgb(42, 161, 152),
|
20
24
|
_ => color::Rgb(255, 255, 255),
|
21
25
|
}
|
22
26
|
}
|
@@ -189,7 +189,7 @@ impl Row {
|
|
189
189
|
fn highlight_char(
|
190
190
|
&mut self,
|
191
191
|
index: &mut usize,
|
192
|
-
opts: HighlightingOptions,
|
192
|
+
opts: &HighlightingOptions,
|
193
193
|
c: char,
|
194
194
|
chars: &[char],
|
195
195
|
) -> bool {
|
@@ -217,7 +217,7 @@ impl Row {
|
|
217
217
|
fn highlight_comment(
|
218
218
|
&mut self,
|
219
219
|
index: &mut usize,
|
220
|
-
opts: HighlightingOptions,
|
220
|
+
opts: &HighlightingOptions,
|
221
221
|
c: char,
|
222
222
|
chars: &[char],
|
223
223
|
) -> bool {
|
@@ -238,7 +238,7 @@ impl Row {
|
|
238
238
|
fn highlight_string(
|
239
239
|
&mut self,
|
240
240
|
index: &mut usize,
|
241
|
-
opts: HighlightingOptions,
|
241
|
+
opts: &HighlightingOptions,
|
242
242
|
c: char,
|
243
243
|
chars: &[char],
|
244
244
|
) -> bool {
|
@@ -264,7 +264,7 @@ impl Row {
|
|
264
264
|
fn highlight_number(
|
265
265
|
&mut self,
|
266
266
|
index: &mut usize,
|
267
|
-
opts: HighlightingOptions,
|
267
|
+
opts: &HighlightingOptions,
|
268
268
|
c: char,
|
269
269
|
chars: &[char],
|
270
270
|
) -> bool {
|
@@ -291,7 +291,7 @@ impl Row {
|
|
291
291
|
}
|
292
292
|
false
|
293
293
|
}
|
294
|
-
pub fn highlight(&mut self, opts: HighlightingOptions, word: Option<&str>) {
|
294
|
+
pub fn highlight(&mut self, opts: &HighlightingOptions, word: Option<&str>) {
|
295
295
|
self.highlighting = Vec::new();
|
296
296
|
let chars: Vec<char> = self.string.chars().collect();
|
297
297
|
let mut index = 0;
|
We had to jump through a surprisingly large number of loops to get this to work. So what happened?
When we added a more complex data structure like Vec<String>
to
HighlightingOptions
, it’s no longer sensible to copy HighlightingOptions
around all the time, which was the case before. Also, we can no longer let the
compiler derive the Copy
trait for us, so we needed to change a few things in
our code to make sure that from now on, only a reference to
HighlightingOptions
is being passed around.
Now that we have the keywords available, let’s highlight them. We start with the primary keywords first.
@@ -127,4 +127,10 @@ impl HighlightingOptions {
|
|
127
127
|
pub fn comments(&self) -> bool {
|
128
128
|
self.comments
|
129
129
|
}
|
130
|
+
pub fn primary_keywords(&self) -> &Vec<String> {
|
131
|
+
&self.primary_keywords
|
132
|
+
}
|
133
|
+
pub fn secondary_keywords(&self) -> &Vec<String> {
|
134
|
+
&self.secondary_keywords
|
135
|
+
}
|
130
136
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
use termion::color;
|
2
|
-
#[derive(PartialEq)]
|
2
|
+
#[derive(PartialEq, Clone, Copy)]
|
3
3
|
pub enum Type {
|
4
4
|
None,
|
5
5
|
Number,
|
@@ -12,7 +12,7 @@ pub enum Type {
|
|
12
12
|
}
|
13
13
|
|
14
14
|
impl Type {
|
15
|
-
pub fn to_color(
|
15
|
+
pub fn to_color(self) -> impl color::Color {
|
16
16
|
match self {
|
17
17
|
Type::Number => color::Rgb(220, 163, 163),
|
18
18
|
Type::Match => color::Rgb(38, 139, 210),
|
@@ -186,6 +186,45 @@ impl Row {
|
|
186
186
|
}
|
187
187
|
}
|
188
188
|
|
189
|
+
fn highlight_str(
|
190
|
+
&mut self,
|
191
|
+
index: &mut usize,
|
192
|
+
substring: &str,
|
193
|
+
chars: &[char],
|
194
|
+
hl_type: highlighting::Type,
|
195
|
+
) -> bool {
|
196
|
+
if substring.is_empty() {
|
197
|
+
return false;
|
198
|
+
}
|
199
|
+
for (substring_index, c) in substring.chars().enumerate() {
|
200
|
+
if let Some(next_char) = chars.get(index.saturating_add(substring_index)) {
|
201
|
+
if *next_char != c {
|
202
|
+
return false;
|
203
|
+
}
|
204
|
+
} else {
|
205
|
+
return false;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
for _ in 0..substring.len() {
|
209
|
+
self.highlighting.push(hl_type);
|
210
|
+
*index += 1;
|
211
|
+
}
|
212
|
+
true
|
213
|
+
}
|
214
|
+
fn highlight_primary_keywords(
|
215
|
+
&mut self,
|
216
|
+
index: &mut usize,
|
217
|
+
opts: &HighlightingOptions,
|
218
|
+
chars: &[char],
|
219
|
+
) -> bool {
|
220
|
+
for word in opts.primary_keywords() {
|
221
|
+
if self.highlight_str(index, word, chars, highlighting::Type::PrimaryKeywords) {
|
222
|
+
return true;
|
223
|
+
}
|
224
|
+
}
|
225
|
+
false
|
226
|
+
}
|
227
|
+
|
189
228
|
fn highlight_char(
|
190
229
|
&mut self,
|
191
230
|
index: &mut usize,
|
@@ -298,6 +337,7 @@ impl Row {
|
|
298
337
|
while let Some(c) = chars.get(index) {
|
299
338
|
if self.highlight_char(&mut index, opts, *c, &chars)
|
300
339
|
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
340
|
+
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|
301
341
|
|| self.highlight_string(&mut index, opts, *c, &chars)
|
302
342
|
|| self.highlight_number(&mut index, opts, *c, &chars)
|
303
343
|
{
|
To make keyword highlighting work, we are building a new function called
highlight_str
, which is responsible for highlighting a substring with a given
type. It does this by comparing every character after the current character with
the given string. If the whole string is matched, self.highlighting
is updated
with the right highlighting type. If one character is off, the whole
highlighting for this string is aborted.
This means that we need to pass around highlighting::Type
, but instead of
worrying about references and borrowing, we are simply deriving Copy
and
Clone
for that type and simply copy it around. Since we have no plans for
Type
to grow, this is a sensible decision for now.
If you start hecto
now, you will see that primary keywords are highlighted.
However, there is still a bug: We don’t want the in
of String
to be
highlighted, keywords need to be preceded and followed by a separator. Let’s fix
that
@@ -217,7 +217,22 @@ impl Row {
|
|
217
217
|
opts: &HighlightingOptions,
|
218
218
|
chars: &[char],
|
219
219
|
) -> bool {
|
220
|
+
if *index > 0 {
|
221
|
+
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
222
|
+
let prev_char = chars[*index - 1];
|
223
|
+
if !is_separator(prev_char) {
|
224
|
+
return false;
|
225
|
+
}
|
226
|
+
}
|
220
227
|
for word in opts.primary_keywords() {
|
228
|
+
if *index < chars.len().saturating_sub(word.len()) {
|
229
|
+
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
230
|
+
let next_char = chars[*index + word.len()];
|
231
|
+
if !is_separator(next_char) {
|
232
|
+
continue;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
221
236
|
if self.highlight_str(index, word, chars, highlighting::Type::PrimaryKeywords) {
|
222
237
|
return true;
|
223
238
|
}
|
@@ -299,7 +314,6 @@ impl Row {
|
|
299
314
|
}
|
300
315
|
false
|
301
316
|
}
|
302
|
-
|
303
317
|
fn highlight_number(
|
304
318
|
&mut self,
|
305
319
|
index: &mut usize,
|
@@ -311,7 +325,7 @@ impl Row {
|
|
311
325
|
if *index > 0 {
|
312
326
|
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
313
327
|
let prev_char = chars[*index - 1];
|
314
|
-
if !
|
328
|
+
if !is_separator(prev_char) {
|
315
329
|
return false;
|
316
330
|
}
|
317
331
|
}
|
@@ -349,3 +363,7 @@ impl Row {
|
|
349
363
|
self.highlight_match(word);
|
350
364
|
}
|
351
365
|
}
|
366
|
+
|
367
|
+
fn is_separator(c: char) -> bool {
|
368
|
+
c.is_ascii_punctuation() || c.is_ascii_whitespace()
|
369
|
+
}
|
Now, let’s try and highlight secondary keywords as well.
@@ -211,11 +211,12 @@ impl Row {
|
|
211
211
|
}
|
212
212
|
true
|
213
213
|
}
|
214
|
-
fn
|
214
|
+
fn highlight_keywords(
|
215
215
|
&mut self,
|
216
216
|
index: &mut usize,
|
217
|
-
opts: &HighlightingOptions,
|
218
217
|
chars: &[char],
|
218
|
+
keywords: &[String],
|
219
|
+
hl_type: highlighting::Type,
|
219
220
|
) -> bool {
|
220
221
|
if *index > 0 {
|
221
222
|
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
@@ -224,7 +225,7 @@ impl Row {
|
|
224
225
|
return false;
|
225
226
|
}
|
226
227
|
}
|
227
|
-
for word in
|
228
|
+
for word in keywords {
|
228
229
|
if *index < chars.len().saturating_sub(word.len()) {
|
229
230
|
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
230
231
|
let next_char = chars[*index + word.len()];
|
@@ -233,13 +234,40 @@ impl Row {
|
|
233
234
|
}
|
234
235
|
}
|
235
236
|
|
236
|
-
if self.highlight_str(index, word, chars,
|
237
|
+
if self.highlight_str(index, &word, chars, hl_type) {
|
237
238
|
return true;
|
238
239
|
}
|
239
240
|
}
|
240
241
|
false
|
241
242
|
}
|
242
243
|
|
244
|
+
fn highlight_primary_keywords(
|
245
|
+
&mut self,
|
246
|
+
index: &mut usize,
|
247
|
+
opts: &HighlightingOptions,
|
248
|
+
chars: &[char],
|
249
|
+
) -> bool {
|
250
|
+
self.highlight_keywords(
|
251
|
+
index,
|
252
|
+
chars,
|
253
|
+
opts.primary_keywords(),
|
254
|
+
highlighting::Type::PrimaryKeywords,
|
255
|
+
)
|
256
|
+
}
|
257
|
+
fn highlight_secondary_keywords(
|
258
|
+
&mut self,
|
259
|
+
index: &mut usize,
|
260
|
+
opts: &HighlightingOptions,
|
261
|
+
chars: &[char],
|
262
|
+
) -> bool {
|
263
|
+
self.highlight_keywords(
|
264
|
+
index,
|
265
|
+
chars,
|
266
|
+
opts.secondary_keywords(),
|
267
|
+
highlighting::Type::SecondaryKeywords,
|
268
|
+
)
|
269
|
+
}
|
270
|
+
|
243
271
|
fn highlight_char(
|
244
272
|
&mut self,
|
245
273
|
index: &mut usize,
|
@@ -352,6 +380,7 @@ impl Row {
|
|
352
380
|
if self.highlight_char(&mut index, opts, *c, &chars)
|
353
381
|
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
354
382
|
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|
383
|
+
|| self.highlight_secondary_keywords(&mut index, &opts, &chars)
|
355
384
|
|| self.highlight_string(&mut index, opts, *c, &chars)
|
356
385
|
|| self.highlight_number(&mut index, opts, *c, &chars)
|
357
386
|
{
|
We have now extracted the core of `highlight_primary_keywords_ into a more generic function which highlights a given word with a given type if it’s surrounded by separators.
When you open your editor now, you should be able to see primary and secondary keywords highlighted.
Colorful multi-line comments
Okay, we have one last feature to implement: multi-line comment highlighting.
Let’s start by adding the appropriate entry to HighlightingOptions
and Type
.
@@ -9,6 +9,7 @@ pub struct HighlightingOptions {
|
|
9
9
|
strings: bool,
|
10
10
|
characters: bool,
|
11
11
|
comments: bool,
|
12
|
+
multiline_comments: bool,
|
12
13
|
primary_keywords: Vec<String>,
|
13
14
|
secondary_keywords: Vec<String>,
|
14
15
|
}
|
@@ -38,6 +39,7 @@ impl FileType {
|
|
38
39
|
strings: true,
|
39
40
|
characters: true,
|
40
41
|
comments: true,
|
42
|
+
multiline_comments: true,
|
41
43
|
primary_keywords: vec![
|
42
44
|
"as".to_string(),
|
43
45
|
"break".to_string(),
|
@@ -133,4 +135,7 @@ impl HighlightingOptions {
|
|
133
135
|
pub fn secondary_keywords(&self) -> &Vec<String> {
|
134
136
|
&self.secondary_keywords
|
135
137
|
}
|
138
|
+
pub fn multiline_comments(&self) -> bool {
|
139
|
+
self.multiline_comments
|
140
|
+
}
|
136
141
|
}
|
@@ -7,6 +7,7 @@ pub enum Type {
|
|
7
7
|
String,
|
8
8
|
Character,
|
9
9
|
Comment,
|
10
|
+
MultilineComment,
|
10
11
|
PrimaryKeywords,
|
11
12
|
SecondaryKeywords,
|
12
13
|
}
|
@@ -18,7 +19,7 @@ impl Type {
|
|
18
19
|
Type::Match => color::Rgb(38, 139, 210),
|
19
20
|
Type::String => color::Rgb(211, 54, 130),
|
20
21
|
Type::Character => color::Rgb(108, 113, 196),
|
21
|
-
Type::Comment => color::Rgb(133, 153, 0),
|
22
|
+
Type::Comment | Type::MultilineComment => color::Rgb(133, 153, 0),
|
22
23
|
Type::PrimaryKeywords => color::Rgb(181, 137, 0),
|
23
24
|
Type::SecondaryKeywords => color::Rgb(42, 161, 152),
|
24
25
|
_ => color::Rgb(255, 255, 255),
|
We’ll highlight multi-line comments to be the same color as single-line comments. Now let’s do the highlighting. We won’t worry about multiple lines just yet.
@@ -316,6 +316,33 @@ impl Row {
|
|
316
316
|
}
|
317
317
|
false
|
318
318
|
}
|
319
|
+
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
320
|
+
fn highlight_multiline_comment(
|
321
|
+
&mut self,
|
322
|
+
index: &mut usize,
|
323
|
+
opts: &HighlightingOptions,
|
324
|
+
c: char,
|
325
|
+
chars: &[char],
|
326
|
+
) -> bool {
|
327
|
+
if opts.comments() && c == '/' && *index < chars.len() {
|
328
|
+
if let Some(next_char) = chars.get(index.saturating_add(1)) {
|
329
|
+
if *next_char == '*' {
|
330
|
+
let closing_index =
|
331
|
+
if let Some(closing_index) = self.string[*index + 2..].find("*/") {
|
332
|
+
*index + closing_index + 4
|
333
|
+
} else {
|
334
|
+
chars.len()
|
335
|
+
};
|
336
|
+
for _ in *index..closing_index {
|
337
|
+
self.highlighting.push(highlighting::Type::MultilineComment);
|
338
|
+
*index += 1;
|
339
|
+
}
|
340
|
+
return true;
|
341
|
+
}
|
342
|
+
};
|
343
|
+
}
|
344
|
+
false
|
345
|
+
}
|
319
346
|
|
320
347
|
fn highlight_string(
|
321
348
|
&mut self,
|
@@ -379,6 +406,7 @@ impl Row {
|
|
379
406
|
while let Some(c) = chars.get(index) {
|
380
407
|
if self.highlight_char(&mut index, opts, *c, &chars)
|
381
408
|
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
409
|
+
|| self.highlight_multiline_comment(&mut index, &opts, *c, &chars)
|
382
410
|
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|
383
411
|
|| self.highlight_secondary_keywords(&mut index, &opts, &chars)
|
384
412
|
|| self.highlight_string(&mut index, opts, *c, &chars)
|
This is essentially a combination of highlighting strings and highlighting
single-line comments. If we are on a /*
, we use find
to get the index of the
closing */
. Since theoretically we could have multiple comments in the same
line, we only search from the current index position forward in the string.
We highlight the whole comment, plus the 4 characters needed by the opening and closing characters of the comment.
Now, how do we highlight multiple rows? The strategy we will be using is that we
pass the information if we ended on an unclosed comment back to document
, so
that document
can highlight the next row
differently, if needed.
@@ -20,7 +20,7 @@ impl Document {
|
|
20
20
|
let mut rows = Vec::new();
|
21
21
|
for value in contents.lines() {
|
22
22
|
let mut row = Row::from(value);
|
23
|
-
row.highlight(&file_type.highlighting_options(), None);
|
23
|
+
row.highlight(&file_type.highlighting_options(), None, false);
|
24
24
|
rows.push(row);
|
25
25
|
}
|
26
26
|
Ok(Self {
|
@@ -53,8 +53,8 @@ impl Document {
|
|
53
53
|
#[allow(clippy::indexing_slicing)]
|
54
54
|
let current_row = &mut self.rows[at.y];
|
55
55
|
let mut new_row = current_row.split(at.x);
|
56
|
-
current_row.highlight(&self.file_type.highlighting_options(), None);
|
57
|
-
new_row.highlight(&self.file_type.highlighting_options(), None);
|
56
|
+
current_row.highlight(&self.file_type.highlighting_options(), None, false);
|
57
|
+
new_row.highlight(&self.file_type.highlighting_options(), None, false);
|
58
58
|
#[allow(clippy::integer_arithmetic)]
|
59
59
|
self.rows.insert(at.y + 1, new_row);
|
60
60
|
}
|
@@ -70,13 +70,13 @@ impl Document {
|
|
70
70
|
if at.y == self.rows.len() {
|
71
71
|
let mut row = Row::default();
|
72
72
|
row.insert(0, c);
|
73
|
-
row.highlight(&self.file_type.highlighting_options(), None);
|
73
|
+
row.highlight(&self.file_type.highlighting_options(), None, false);
|
74
74
|
self.rows.push(row);
|
75
75
|
} else {
|
76
76
|
#[allow(clippy::indexing_slicing)]
|
77
77
|
let row = &mut self.rows[at.y];
|
78
78
|
row.insert(at.x, c);
|
79
|
-
row.highlight(&self.file_type.highlighting_options(), None);
|
79
|
+
row.highlight(&self.file_type.highlighting_options(), None, false);
|
80
80
|
}
|
81
81
|
}
|
82
82
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
@@ -90,21 +90,26 @@ impl Document {
|
|
90
90
|
let next_row = self.rows.remove(at.y + 1);
|
91
91
|
let row = &mut self.rows[at.y];
|
92
92
|
row.append(&next_row);
|
93
|
-
row.highlight(&self.file_type.highlighting_options(), None);
|
93
|
+
row.highlight(&self.file_type.highlighting_options(), None, false);
|
94
94
|
} else {
|
95
95
|
let row = &mut self.rows[at.y];
|
96
96
|
row.delete(at.x);
|
97
|
-
row.highlight(&self.file_type.highlighting_options(), None);
|
97
|
+
row.highlight(&self.file_type.highlighting_options(), None, false);
|
98
98
|
}
|
99
99
|
}
|
100
100
|
pub fn save(&mut self) -> Result<(), Error> {
|
101
101
|
if let Some(file_name) = &self.file_name {
|
102
102
|
let mut file = fs::File::create(file_name)?;
|
103
103
|
self.file_type = FileType::from(file_name);
|
104
|
+
let mut start_with_comment = false;
|
104
105
|
for row in &mut self.rows {
|
105
106
|
file.write_all(row.as_bytes())?;
|
106
107
|
file.write_all(b"\n")?;
|
107
|
-
row.highlight(
|
108
|
+
start_with_comment = row.highlight(
|
109
|
+
&self.file_type.highlighting_options(),
|
110
|
+
None,
|
111
|
+
start_with_comment,
|
112
|
+
);
|
108
113
|
}
|
109
114
|
self.dirty = false;
|
110
115
|
}
|
@@ -150,8 +155,13 @@ impl Document {
|
|
150
155
|
None
|
151
156
|
}
|
152
157
|
pub fn highlight(&mut self, word: Option<&str>) {
|
158
|
+
let mut start_with_comment = false;
|
153
159
|
for row in &mut self.rows {
|
154
|
-
row.highlight(
|
160
|
+
start_with_comment = row.highlight(
|
161
|
+
&self.file_type.highlighting_options(),
|
162
|
+
word,
|
163
|
+
start_with_comment,
|
164
|
+
);
|
155
165
|
}
|
156
166
|
}
|
157
167
|
}
|
@@ -399,14 +399,36 @@ impl Row {
|
|
399
399
|
}
|
400
400
|
false
|
401
401
|
}
|
402
|
-
|
402
|
+
#[allow(clippy::indexing_slicing, clippy::integer_arithmetic)]
|
403
|
+
pub fn highlight(
|
404
|
+
&mut self,
|
405
|
+
opts: &HighlightingOptions,
|
406
|
+
word: Option<&str>,
|
407
|
+
start_with_comment: bool,
|
408
|
+
) -> bool {
|
403
409
|
self.highlighting = Vec::new();
|
404
410
|
let chars: Vec<char> = self.string.chars().collect();
|
405
411
|
let mut index = 0;
|
412
|
+
let mut in_ml_comment = start_with_comment;
|
413
|
+
if in_ml_comment {
|
414
|
+
let closing_index = if let Some(closing_index) = self.string.find("*/") {
|
415
|
+
closing_index + 2
|
416
|
+
} else {
|
417
|
+
chars.len()
|
418
|
+
};
|
419
|
+
for _ in 0..closing_index {
|
420
|
+
self.highlighting.push(highlighting::Type::MultilineComment);
|
421
|
+
}
|
422
|
+
index = closing_index;
|
423
|
+
}
|
406
424
|
while let Some(c) = chars.get(index) {
|
425
|
+
if self.highlight_multiline_comment(&mut index, &opts, *c, &chars) {
|
426
|
+
in_ml_comment = true;
|
427
|
+
continue;
|
428
|
+
}
|
429
|
+
in_ml_comment = false;
|
407
430
|
if self.highlight_char(&mut index, opts, *c, &chars)
|
408
431
|
|| self.highlight_comment(&mut index, opts, *c, &chars)
|
409
|
-
|| self.highlight_multiline_comment(&mut index, &opts, *c, &chars)
|
410
432
|
|| self.highlight_primary_keywords(&mut index, &opts, &chars)
|
411
433
|
|| self.highlight_secondary_keywords(&mut index, &opts, &chars)
|
412
434
|
|| self.highlight_string(&mut index, opts, *c, &chars)
|
@@ -418,6 +440,10 @@ impl Row {
|
|
418
440
|
index += 1;
|
419
441
|
}
|
420
442
|
self.highlight_match(word);
|
443
|
+
if in_ml_comment && &self.string[self.string.len().saturating_sub(2)..] != "*/" {
|
444
|
+
return true;
|
445
|
+
}
|
446
|
+
false
|
421
447
|
}
|
422
448
|
}
|
423
449
|
|
We have now changed the signature of highlight
: It receives a new boolean, and
it returns a boolean. The boolean it receives is true
when the highlighting
should start within a multiline comment. If that is the case, we look for the
closing index before we enter the highlighting loop and highlight everything
until then as a mutli line comment. Similar to what we are doing in
highlight_multiline_comment
, we go up until the end of the line if we can’t
find the closing */
.
During highlighting, we are now keeping track if the last thing we highlighted
was a multiline comment. If we end the loop on a multiline comment, we check if
the last thing we saw was the ending of a multiline comment, as we want to
return false
if we are out of a multi line comment, and true
if the multi
line comment we are in has not been closed yet.
You can try saving a file with multi line comments now, or you can enter a multi line comment and then trigger a search to see the correct highlighting. Let’s now make sure that the highlighting is also correct in all other cases.
@@ -17,10 +17,12 @@ impl Document {
|
|
17
17
|
pub fn open(filename: &str) -> Result<Self, std::io::Error> {
|
18
18
|
let contents = fs::read_to_string(filename)?;
|
19
19
|
let file_type = FileType::from(filename);
|
20
|
+
let mut start_with_comment = false;
|
20
21
|
let mut rows = Vec::new();
|
21
22
|
for value in contents.lines() {
|
22
23
|
let mut row = Row::from(value);
|
23
|
-
|
24
|
+
start_with_comment =
|
25
|
+
row.highlight(&file_type.highlighting_options(), None, start_with_comment);
|
24
26
|
rows.push(row);
|
25
27
|
}
|
26
28
|
Ok(Self {
|
@@ -52,9 +54,7 @@ impl Document {
|
|
52
54
|
}
|
53
55
|
#[allow(clippy::indexing_slicing)]
|
54
56
|
let current_row = &mut self.rows[at.y];
|
55
|
-
let
|
57
|
+
let new_row = current_row.split(at.x);
|
56
|
-
current_row.highlight(&self.file_type.highlighting_options(), None, false);
|
57
|
-
new_row.highlight(&self.file_type.highlighting_options(), None, false);
|
58
58
|
#[allow(clippy::integer_arithmetic)]
|
59
59
|
self.rows.insert(at.y + 1, new_row);
|
60
60
|
}
|
@@ -65,19 +65,16 @@ impl Document {
|
|
65
65
|
self.dirty = true;
|
66
66
|
if c == '\n' {
|
67
67
|
self.insert_newline(at);
|
68
|
-
|
68
|
+
} else if at.y == self.rows.len() {
|
69
|
-
}
|
70
|
-
if at.y == self.rows.len() {
|
71
69
|
let mut row = Row::default();
|
72
70
|
row.insert(0, c);
|
73
|
-
row.highlight(&self.file_type.highlighting_options(), None, false);
|
74
71
|
self.rows.push(row);
|
75
72
|
} else {
|
76
73
|
#[allow(clippy::indexing_slicing)]
|
77
74
|
let row = &mut self.rows[at.y];
|
78
75
|
row.insert(at.x, c);
|
79
|
-
row.highlight(&self.file_type.highlighting_options(), None, false);
|
80
76
|
}
|
77
|
+
self.highlight(None);
|
81
78
|
}
|
82
79
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
83
80
|
pub fn delete(&mut self, at: &Position) {
|
@@ -90,12 +87,11 @@ impl Document {
|
|
90
87
|
let next_row = self.rows.remove(at.y + 1);
|
91
88
|
let row = &mut self.rows[at.y];
|
92
89
|
row.append(&next_row);
|
93
|
-
row.highlight(&self.file_type.highlighting_options(), None, false);
|
94
90
|
} else {
|
95
91
|
let row = &mut self.rows[at.y];
|
96
92
|
row.delete(at.x);
|
97
|
-
row.highlight(&self.file_type.highlighting_options(), None, false);
|
98
93
|
}
|
94
|
+
self.highlight(None);
|
99
95
|
}
|
100
96
|
pub fn save(&mut self) -> Result<(), Error> {
|
101
97
|
if let Some(file_name) = &self.file_name {
|
We have now restructured the code a bit so that the whole document is re-highlighted on any update or delete operation. If you check it now, you should notice two things: First, it works! Very satisfying! Second, the performance is abysmal. Not satisfying!
The reason is, of course, that we are re-highlighting the whole document all the time, on every change, to make sure the highlighting is correct everywhere and in every case. Let’s try to improve the performance by highlighting the document less often.
@@ -17,13 +17,9 @@ impl Document {
|
|
17
17
|
pub fn open(filename: &str) -> Result<Self, std::io::Error> {
|
18
18
|
let contents = fs::read_to_string(filename)?;
|
19
19
|
let file_type = FileType::from(filename);
|
20
|
-
let mut start_with_comment = false;
|
21
20
|
let mut rows = Vec::new();
|
22
21
|
for value in contents.lines() {
|
23
|
-
|
22
|
+
rows.push(Row::from(value));
|
24
|
-
start_with_comment =
|
25
|
-
row.highlight(&file_type.highlighting_options(), None, start_with_comment);
|
26
|
-
rows.push(row);
|
27
23
|
}
|
28
24
|
Ok(Self {
|
29
25
|
rows,
|
@@ -74,7 +70,6 @@ impl Document {
|
|
74
70
|
let row = &mut self.rows[at.y];
|
75
71
|
row.insert(at.x, c);
|
76
72
|
}
|
77
|
-
self.highlight(None);
|
78
73
|
}
|
79
74
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
80
75
|
pub fn delete(&mut self, at: &Position) {
|
@@ -91,21 +86,14 @@ impl Document {
|
|
91
86
|
let row = &mut self.rows[at.y];
|
92
87
|
row.delete(at.x);
|
93
88
|
}
|
94
|
-
self.highlight(None);
|
95
89
|
}
|
96
90
|
pub fn save(&mut self) -> Result<(), Error> {
|
97
91
|
if let Some(file_name) = &self.file_name {
|
98
92
|
let mut file = fs::File::create(file_name)?;
|
99
93
|
self.file_type = FileType::from(file_name);
|
100
|
-
let mut start_with_comment = false;
|
101
94
|
for row in &mut self.rows {
|
102
95
|
file.write_all(row.as_bytes())?;
|
103
96
|
file.write_all(b"\n")?;
|
104
|
-
start_with_comment = row.highlight(
|
105
|
-
&self.file_type.highlighting_options(),
|
106
|
-
None,
|
107
|
-
start_with_comment,
|
108
|
-
);
|
109
97
|
}
|
110
98
|
self.dirty = false;
|
111
99
|
}
|
@@ -150,9 +138,19 @@ impl Document {
|
|
150
138
|
}
|
151
139
|
None
|
152
140
|
}
|
153
|
-
pub fn highlight(&mut self, word: Option
|
141
|
+
pub fn highlight(&mut self, word: &Option<String>, until: Option<usize>) {
|
154
142
|
let mut start_with_comment = false;
|
155
|
-
|
143
|
+
let until = if let Some(until) = until {
|
144
|
+
if until.saturating_add(1) < self.rows.len() {
|
145
|
+
until.saturating_add(1)
|
146
|
+
} else {
|
147
|
+
self.rows.len()
|
148
|
+
}
|
149
|
+
} else {
|
150
|
+
self.rows.len()
|
151
|
+
};
|
152
|
+
#[allow(clippy::indexing_slicing)]
|
153
|
+
for row in &mut self.rows[..until] {
|
156
154
|
start_with_comment = row.highlight(
|
157
155
|
&self.file_type.highlighting_options(),
|
158
156
|
word,
|
@@ -45,6 +45,7 @@ pub struct Editor {
|
|
45
45
|
document: Document,
|
46
46
|
status_message: StatusMessage,
|
47
47
|
quit_times: u8,
|
48
|
+
highlighted_word: Option<String>,
|
48
49
|
}
|
49
50
|
|
50
51
|
impl Editor {
|
@@ -86,16 +87,25 @@ impl Editor {
|
|
86
87
|
offset: Position::default(),
|
87
88
|
status_message: StatusMessage::from(initial_status),
|
88
89
|
quit_times: QUIT_TIMES,
|
90
|
+
highlighted_word: None,
|
89
91
|
}
|
90
92
|
}
|
91
93
|
|
92
|
-
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
94
|
+
fn refresh_screen(&mut self) -> Result<(), std::io::Error> {
|
93
95
|
Terminal::cursor_hide();
|
94
96
|
Terminal::cursor_position(&Position::default());
|
95
97
|
if self.should_quit {
|
96
98
|
Terminal::clear_screen();
|
97
99
|
println!("Goodbye.\r");
|
98
100
|
} else {
|
101
|
+
self.document.highlight(
|
102
|
+
&self.highlighted_word,
|
103
|
+
Some(
|
104
|
+
self.offset
|
105
|
+
.y
|
106
|
+
.saturating_add(self.terminal.size().height as usize),
|
107
|
+
),
|
108
|
+
);
|
99
109
|
self.draw_rows();
|
100
110
|
self.draw_status_bar();
|
101
111
|
self.draw_message_bar();
|
@@ -150,7 +160,7 @@ impl Editor {
|
|
150
160
|
} else if moved {
|
151
161
|
editor.move_cursor(Key::Left);
|
152
162
|
}
|
153
|
-
editor.
|
163
|
+
editor.highlighted_word = Some(query.to_string());
|
154
164
|
},
|
155
165
|
)
|
156
166
|
.unwrap_or(None);
|
@@ -159,7 +169,7 @@ impl Editor {
|
|
159
169
|
self.cursor_position = old_position;
|
160
170
|
self.scroll();
|
161
171
|
}
|
162
|
-
self.
|
172
|
+
self.highlighted_word = None;
|
163
173
|
}
|
164
174
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
165
175
|
let pressed_key = Terminal::read_key()?;
|
@@ -165,7 +165,7 @@ impl Row {
|
|
165
165
|
None
|
166
166
|
}
|
167
167
|
|
168
|
-
fn highlight_match(&mut self, word: Option
|
168
|
+
fn highlight_match(&mut self, word: &Option<String>) {
|
169
169
|
if let Some(word) = word {
|
170
170
|
if word.is_empty() {
|
171
171
|
return;
|
@@ -403,7 +403,7 @@ impl Row {
|
|
403
403
|
pub fn highlight(
|
404
404
|
&mut self,
|
405
405
|
opts: &HighlightingOptions,
|
406
|
-
word: Option
|
406
|
+
word: &Option<String>,
|
407
407
|
start_with_comment: bool,
|
408
408
|
) -> bool {
|
409
409
|
self.highlighting = Vec::new();
|
We have now removed all highlighting directly within Row
, as we are now doing
the highlighting controlled by the editor
. To do that, we have added a new
parameter to highlight
in row
: until
, which denotes the index of the last
line for which the highlighting should be calculated. We know that the
highlighting of everything on screen always depends on the earlier rows in the
document, but not the rows below.
We are setting the highlighting in the editor’s refresh_screen
method. This
also means that we have to change how search results are being displayed, as
refresh_screen
is always called when we conduct a search, which means that we
would overwrite the highlighting of a word during refresh_screen
, rendering
our highlighting of the match useless. We solve this by storing the currently
highlighted word as part of the editor
struct, setting and resetting this
during the search.
You should now be able to verify that the performance is now much, much better. The higher up the user is in the file, the better the performance will be, as we will be only highlighting rows up until the end of the current screen. But if the user is at the bottom of the file, we are still doing a lot of highlighting of all the rows above.
It’s true that we need to take the rows above into account when highlighting to understand whether or not we are in a multi-line comment or not. However, there is no need to actually re-do the highlighting for all these lines.
Let’s make sure that only the line which has been edited is being highlighted, as well as all following lines (to make sure multi line comments still work). Together with the recent change, this means that we are highlighting only the lines between the current line and the last line on screen.
@@ -70,6 +70,14 @@ impl Document {
|
|
70
70
|
let row = &mut self.rows[at.y];
|
71
71
|
row.insert(at.x, c);
|
72
72
|
}
|
73
|
+
self.unhighlight_rows(at.y);
|
74
|
+
}
|
75
|
+
|
76
|
+
fn unhighlight_rows(&mut self, start: usize) {
|
77
|
+
let start = start.saturating_sub(1);
|
78
|
+
for row in self.rows.iter_mut().skip(start) {
|
79
|
+
row.is_highlighted = false;
|
80
|
+
}
|
73
81
|
}
|
74
82
|
#[allow(clippy::integer_arithmetic, clippy::indexing_slicing)]
|
75
83
|
pub fn delete(&mut self, at: &Position) {
|
@@ -86,6 +94,7 @@ impl Document {
|
|
86
94
|
let row = &mut self.rows[at.y];
|
87
95
|
row.delete(at.x);
|
88
96
|
}
|
97
|
+
self.unhighlight_rows(at.y);
|
89
98
|
}
|
90
99
|
pub fn save(&mut self) -> Result<(), Error> {
|
91
100
|
if let Some(file_name) = &self.file_name {
|
@@ -9,6 +9,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|
9
9
|
pub struct Row {
|
10
10
|
string: String,
|
11
11
|
highlighting: Vec<highlighting::Type>,
|
12
|
+
pub is_highlighted: bool,
|
12
13
|
len: usize,
|
13
14
|
}
|
14
15
|
|
@@ -17,6 +18,7 @@ impl From<&str> for Row {
|
|
17
18
|
Self {
|
18
19
|
string: String::from(slice),
|
19
20
|
highlighting: Vec::new(),
|
21
|
+
is_highlighted: false,
|
20
22
|
len: slice.graphemes(true).count(),
|
21
23
|
}
|
22
24
|
}
|
@@ -118,9 +120,11 @@ impl Row {
|
|
118
120
|
|
119
121
|
self.string = row;
|
120
122
|
self.len = length;
|
123
|
+
self.is_highlighted = false;
|
121
124
|
Self {
|
122
125
|
string: splitted_row,
|
123
126
|
len: splitted_length,
|
127
|
+
is_highlighted: false,
|
124
128
|
highlighting: Vec::new(),
|
125
129
|
}
|
126
130
|
}
|
@@ -406,8 +410,19 @@ impl Row {
|
|
406
410
|
word: &Option<String>,
|
407
411
|
start_with_comment: bool,
|
408
412
|
) -> bool {
|
409
|
-
self.highlighting = Vec::new();
|
410
413
|
let chars: Vec<char> = self.string.chars().collect();
|
414
|
+
if self.is_highlighted && word.is_none() {
|
415
|
+
if let Some(hl_type) = self.highlighting.last() {
|
416
|
+
if *hl_type == highlighting::Type::MultilineComment
|
417
|
+
&& self.string.len() > 1
|
418
|
+
&& self.string[self.string.len() - 2..] == *"*/"
|
419
|
+
{
|
420
|
+
return true;
|
421
|
+
}
|
422
|
+
}
|
423
|
+
return false;
|
424
|
+
}
|
425
|
+
self.highlighting = Vec::new();
|
411
426
|
let mut index = 0;
|
412
427
|
let mut in_ml_comment = start_with_comment;
|
413
428
|
if in_ml_comment {
|
@@ -443,6 +458,7 @@ impl Row {
|
|
443
458
|
if in_ml_comment && &self.string[self.string.len().saturating_sub(2)..] != "*/" {
|
444
459
|
return true;
|
445
460
|
}
|
461
|
+
self.is_highlighted = true;
|
446
462
|
false
|
447
463
|
}
|
448
464
|
}
|
We solve this by storing on a row whether or not it is currently properly
highlighted. If highlight
is called on a row which is already highlighted, we
only check if this row ends with an unclosed multiline comment, to determine the
return value of highlight
. If it’s not highlighted, we do the highlighting as
usual and then set is_highlighted
to true
.
We also make sure that is_highlighted
is false
whenever a row is modified or
created.
In our document
, we are then setting is_highlighted
to false
for every row
after and including the row which has been currently edited.
In combination with the previous change, this means that all rows after the current row are marked for re-highlighting whenever we change something, but re-highlighting is only done up until the last row on screen. In combination, this means that when you are typing, at most all the rows currently on the screen are being re-highlighted, which can be done at a much better performance.
To make sure our search results are still displayed, we re-highlight a row even
if it has been highlighted previously in case a word is provided to highlight
.
You’re done!
That’s it! Our text editor is finished. In the appendices, you’ll find some ideas for features you might want to extend the editor with on your own.