8

Build Your Text Editor With Rust! Part 6

 2 years ago
source link: https://medium.com/@otukof/build-your-text-editor-with-rust-part-6-3cff61dc2de5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Build Your Text Editor With Rust! Part 6

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 Clonefor 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:

Search With Arrow Keys

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK