This is the penultimate part of this walk-through. In part 5, we enabled the user to write to the file and save it. In this part, we’ll implement a search feature and in the final part we’ll add syntax highlighting.
Let’s begin by using prompt
to implement a simple search. When the user types a search query and presses Enter
, we’ll loop through all the rows of the file, and if a row contains their query string, we’ll move the cursor to the match:
Recall that prompt!()
returns None
if the user aborted the prompt so we have to check if prompt!()
returned a search keyword. If it did, we loop through all the rows and use .find()
to check if the keyword
provided is in that row. If it is, we set the cursor position to where the query is. Lastly, we set row_offset
so that we are scrolled to the very bottom of the file, which will cause scroll()
to scroll upwards at the next screen refresh so that the matching line will be at the very top of the screen. This way, the user doesn’t have to look all over their screen to find where their cursor jumped to, and where the matching line is.
But there’s a little problem. We assigned a get_render
index to cursor_x
, but cursor_x
is an index into row_content
. If there are tabs to the left of the match, the cursor is going to be in the wrong position. We need to convert the render
index into a row_content
index before assigning it to cursor_x
. Let’s create a get_row_content_x()
function, which is the opposite of the get_render_x()
function we wrote in part 4:
To convert a render_x
into a cursor_x
, we do pretty much the same thing when converting the other way: loop through the chars
of row_content
, calculating the current render_x
value as we go. At the point, when current_render_x
becomes more than the render_x
provided, it means we’ve reached the corresponding cursor_x
. Note that the function would always return cursor_x
as long as the render_x
provided is valid. We return 0
if the function was called on an empty row.
Now let’s call get_row_content_x()
in the find
method:
Finally, let’s map Ctrl-F
to the find
function, and add it to the help message we set in StatusMessage::new()
:
Incremental Search 🔍
Let’s add a feature to our search, similar to how many searches work. We want to support incremental search, meaning the file is searched after each keypress as the user is typing in their search query.
To implement this, we’re going to get prompt!()
to take a callback function as an argument. We’ll have it call this function after each keypress, passing the current search query inputted by the user and the last key they pressed:
We add a new parameter, callback
which takes a function or closure which would be called when there’s an input. Since we don’t want to refactor parts of our code where prompt!()
is called, we add a new pattern to the prompt!
macro and modify the previous one. This enables us to call prompt!()
with either 2 or 3 arguments. The first pattern,
($output:expr,$args:tt) => {
prompt!($output, $args, callback = |&_, _, _| {})
};
, takes 2 arguments and then calls prompt!()
with 3 arguments. The last argument is an empty callback which does nothing. (We use &_
since we don’t want the closure to take ownership of Output
)
Now let’s move the actual searching code from find()
to a new function find_callback()
:
In the callback, we check if the user pressed Enter
or Escape
, in which case they are leaving search mode so we return immediately instead of doing another search. Otherwise, after any other keypress, we do another search for the current query
string. And that’s it for incremental search.
Restore Cursor Position ↩️
When the user presses Escape
to cancel a search, we want the cursor to go back to where it was when they started the search. To do that, we’ll have to save their cursor position and scroll position, and restore those values after the search is cancelled. First, let’s derive
Copy
and Clone
for CursorController
:
Now let’s restore the cursor position:
Search Forward And Backward ♾
The last feature we’d like to add is to allow the user to advance to the next or previous match in the file using the arrow keys. The ↑ and ↓ keys will go to the match above or below the current line respectively, while the ← and → keys will go to the match before or after (respectively) the current match on the same line. We’ll use 2 variables, x_index
and y_index
, to determine how the search would take place. x_index
would show where on the row the search should begin while y_index
would show which row the search should begin. Their corresponding x_direction
and y_direction
will determine whether we should search in the forward or backward direction. We’ll create a new struct
to hold these values:
Now let’s implement vertical scrolling during search:
When the user presses either ArrowUp
or ArrowDown
, we set the direction accordingly. If any other key was pressed, we reset the direction. In the for
loop, we calculate the row_index
. When there’s no direction i.e when the user presses any key apart from Enter
, Esc
, ArrowUp
or ArrowDown
, we reset y_index
and use i
as the index, just as we did when we implemented incremental search above. It gets a bit tricky when a direction is provided.
If ArrowDown
was pressed, we add i
to y_index
and then add 1
. This would get us to the next row, from which we’d start the search. For instance, if the first match was on the second row, we want the next search to start from the third row onward. Since y_index
was assigned index of 1
(in the line y_index = row_index
), we can get the search to start from index of 2
onward by simply adding 1
to y_index. We also add i
to keep increasing the index.
We do the opposite for reverse direction. We subtract i
from y_index
. If it is 0
, it simply means we’re on the very first line with a match and so we return. If it’s greater than 0
, we move to the previous line and start the search from there upward. We also check if the row_index
is valid before using it in get_editor_row
.
Let us now allow the user to move to the next match on the same line:
If we want our search to be on the same line, when we have to keep row_index
as search_index.y_index
when the user presses the various arrow keys. When the user isn’t navigating with the left and right arrow keys, we perform our search like how we did it initially. If the user pressed ArrowRight
then we perform the search from the current x position onward and then we add the amount of characters before the start position. That way we can get the total index. If the user pressed ArrowLeft
, we use .rfind()
to reverse the direction of the search. Note that if no index is returned, then it means we’re currently on the last match on that line (depending on the direction) so we just break
out of the for
loop.
Finally, let’s not forget to update the prompt text to let the user know they can use the arrow keys:
In the final part we would implement syntax highlighting 🎨!