コンテンツにスキップ

Search

Let's use editorPrompt() to implement a minimal search feature. 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.

{{basic-search}}

strstr() comes from <string.h>.

If they pressed Escape to cancel the input prompt, then editorPrompt() returns NULL and we abort the search.

Otherwise, we loop through all the rows of the file. We use strstr() to check if query is a substring of the current row. It returns NULL if there is no match, otherwise it returns a pointer to the matching substring. To convert that into an index that we can set E.cx to, we subtract the row->render pointer from the match pointer, since match is a pointer into the row->render string. Lastly, we set E.rowoff so that we are scrolled to the very bottom of the file, which will cause editorScroll() 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.

There's one problem here. Did you notice what we just did wrong? We assigned a render index to E.cx, but E.cx is an index into chars. 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 chars index before assigning it to E.cx. Let's create an editorRowRxToCx() function, which is the opposite of the editorRowCxToRx() function we wrote in chapter 4, but contains a lot of the same code.

{{rx-to-cx}}

To convert an rx into a cx, we do pretty much the same thing when converting the other way: loop through the chars string, calculating the current rx value (cur_rx) as we go. But instead of stopping when we hit a particular cx value and returning cur_rx, we want to stop when cur_rx hits the given rx value and return cx.

The return statement at the very end is just in case the caller provided an rx that's out of range, which shouldn't happen. The return statement inside the for loop should handle all rx values that are valid indexes into render.

Now let's call editorRowRxToCx() to convert the matched index to a chars index and assign that to E.cx.

{{use-rx-to-cx}}

Finally, let's map Ctrl-F to the editorFind() function, and add it to the help message we set in main().

{{ctrl-f}}

Now, let's make our search feature fancy. We want to support incremental search, meaning the file is searched after each keypress when the user is typing in their search query.

To implement this, we're going to get editorPrompt() 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.

{{prompt-callback}}

The if statements allow the caller to pass NULL for the callback, in case they don't want to use a callback. This is the case when we prompt the user for a filename, so let's pass NULL to editorPrompt() when we do that. We'll also pass NULL to editorPrompt() in editorFind() for now, to get the code to compile.

{{null-callback}}

Now let's move the actual searching code from editorFind() into a function called editorFindCallback(). Obviously this will be our callback function for editorPrompt().

{{incremental-search}}

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.

That's all there is to it. We now have incremental search.

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.

{{restore-cursor}}

If query is NULL, that means they pressed Escape, so in that case we restore the values we saved.

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 previous match, and the and keys will go to the next match.

We'll implement this feature using two static variables in our callback. last_match will contain the index of the row that the last match was on, or -1 if there was no last match. And direction will store the direction of the search: 1 for searching forward, and -1 for searching backward.

{{callback-statics}}

As you can see, we always reset last_match to -1 unless an arrow key was pressed. So we'll only advance to the next or previous match when an arrow key is pressed. You can also see that we always set direction to 1 unless the or key was pressed. So we always search in the forward direction unless the user specifically asks to search backwards from the last match.

If key is '\r' (Enter) or '\x1b' (Escape), that means we're about to leave search mode. So we reset last_match and direction to their initial values to get ready for the next search operation.

Now that we have those variables all set up, let's put them to use.

{{search-arrows}}

current is the index of the current row we are searching. If there was a last match, it starts on the line after (or before, if we're searching backwards). If there wasn't a last match, it starts at the top of the file and searches in the forward direction to find the first match.

The if ... else if causes current to go from the end of the file back to the beginning of the file, or vice versa, to allow a search to "wrap around" the end of a file and continue from the top (or bottom).

When we find a match, we set last_match to current, so that if the user presses the arrow keys, we'll start the next search from that point.

Finally, let's not forget to update the prompt text to let the user know they can use the arrow keys.

{{search-arrows-help}}

In the next chapter, we'll implement syntax highlighting and filetype detection, to complete our text editor.