Это предпоследняя часть данного прохождения. В части 5 мы разрешили пользователю писать в файл и сохранять его. В этой части мы реализуем функцию поиска, а в заключительной части добавим подсветку синтаксиса.

Давайте начнем с использования prompt для реализации простого поиска. Когда пользователь вводит поисковый запрос и нажимает Enter, мы проходим по всем строкам файла, и если строка содержит его строку запроса, мы перемещаем курсор к совпадению:

Напомним, что prompt!() возвращает None, если пользователь прервал приглашение, поэтому мы должны проверить, вернул ли prompt!() ключевое слово для поиска. Если это так, мы перебираем все строки и используем .find(), чтобы проверить, находится ли предоставленный keyword в этой строке. Если это так, мы устанавливаем позицию курсора там, где находится запрос. Наконец, мы устанавливаем row_offset так, чтобы мы прокручивались до самого низа файла, что заставит scroll() прокручиваться вверх при следующем обновлении экрана, так что совпадающая строка будет в самом верху экрана. Таким образом, пользователю не нужно просматривать весь экран, чтобы найти, куда переместился его курсор и где находится совпадающая строка.

Но есть небольшая проблема. Мы присвоили индекс get_render cursor_x, но cursor_x является индексом row_content. Если слева от совпадения есть вкладки, курсор будет в неправильном положении. Нам нужно преобразовать индекс render в индекс row_content, прежде чем назначать его cursor_x. Давайте создадим функцию get_row_content_x(), противоположную функции get_render_x(), которую мы написали в Части 4:

Чтобы преобразовать render_x в cursor_x, мы делаем почти то же самое при обратном преобразовании: перебираем chars из row_content, вычисляя current render_x value по мере продвижения. В тот момент, когда current_render_x становится больше предоставленного render_x, это означает, что мы достигли соответствующего cursor_x. Обратите внимание, что функция всегда будет возвращать cursor_x, если предоставленное render_x допустимо. Мы возвращаем 0, если функция была вызвана для пустой строки.

Теперь вызовем get_row_content_x() в методе find:

Наконец, давайте сопоставим Ctrl-F с функцией find и добавим ее в справочное сообщение, которое мы установили в StatusMessage::new():

Дополнительный поиск 🔍

Давайте добавим в наш поиск функцию, похожую на то, как работает множество поисков. Мы хотим поддерживать добавочный поиск, то есть файл ищется после каждого нажатия клавиши, когда пользователь вводит свой поисковый запрос.

Чтобы реализовать это, мы собираемся заставить prompt!() принимать функцию обратного вызова в качестве аргумента. Мы будем вызывать эту функцию после каждого нажатия клавиши, передавая текущий поисковый запрос, введенный пользователем, и последнюю нажатую клавишу:

Мы добавляем новый параметр callback, который принимает функцию или замыкание, которое будет вызываться при наличии ввода. Поскольку мы не хотим рефакторить части нашего кода, где вызывается prompt!(), мы добавляем новый шаблон в макрос prompt! и модифицируем предыдущий. Это позволяет нам вызывать prompt!() с 2 или 3 аргументами. Первый узор,

($output:expr,$args:tt) => {
    prompt!($output, $args, callback = |&_, _, _| {})
};

, принимает 2 аргумента и затем вызывает prompt!() с 3 аргументами. Последний аргумент — это пустой обратный вызов, который ничего не делает. (Мы используем &_, так как мы не хотим, чтобы замыкание стало владельцем Output)

Теперь давайте переместим фактический код поиска из find() в новую функцию find_callback():

В обратном вызове мы проверяем, нажал ли пользователь Enter или Escape, и в этом случае он выходит из режима поиска, поэтому мы немедленно возвращаемся, вместо того чтобы выполнять новый поиск. В противном случае, после любого другого нажатия клавиши, мы делаем еще один поиск текущей строки query. И это все для пошагового поиска.

Восстановить положение курсора ↩️

Когда пользователь нажимает Escape, чтобы отменить поиск, мы хотим, чтобы курсор вернулся туда, где он был, когда он начал поиск. Для этого нам нужно сохранить их положение курсора и положение прокрутки, а также восстановить эти значения после отмены поиска. Сначала derive Copy и Clone для CursorController:

Теперь восстановим положение курсора:

Поиск вперед и назад ♾

Последняя функция, которую мы хотели бы добавить, — позволить пользователю переходить к следующему или предыдущему совпадению в файле с помощью клавиш со стрелками. Клавиши ↑ и ↓ переходят к совпадению выше или ниже текущей строки соответственно, а клавиши ← и → переходят к совпадению до или после (соответственно) текущего совпадения на та же строка. Мы будем использовать 2 переменные, x_index и y_index, чтобы определить, как будет происходить поиск. x_index показывает, где в строке должен начинаться поиск, а y_index показывает, в какой строке должен начинаться поиск. Их соответствующие x_direction и y_direction будут определять, следует ли нам искать в прямом или обратном направлении. Мы создадим новый struct для хранения этих значений:

Теперь давайте реализуем вертикальную прокрутку во время поиска:

Когда пользователь нажимает ArrowUp или ArrowDown, мы соответствующим образом устанавливаем направление. Если была нажата любая другая клавиша, мы сбрасываем направление. В цикле for мы вычисляем row_index. Когда нет направления т. е. когда пользователь нажимает любую клавишу, кроме Enter, Esc, ArrowUp или ArrowDown, мы сбрасываем y_index и используем i в качестве индекса, как мы делали, когда реализовывали пошаговый поиск выше. Это становится немного сложнее, когда указано направление.

Если была нажата ArrowDown, мы добавляем i к y_index, а затем добавляем 1. Это приведет нас к следующей строке, с которой мы начнем поиск. Например, если первое совпадение было во второй строке, мы хотим, чтобы следующий поиск начинался с третьей строки и далее. Поскольку y_index был присвоен индекс 1 (в строке y_index = row_index), мы можем начать поиск с индекса 2 и далее, просто добавив 1 к y_index. Мы также добавляем i, чтобы продолжать увеличивать индекс.

Мы делаем обратное для обратного направления. Вычитаем i из y_index. Если это 0, это просто означает, что мы находимся на самой первой строке с совпадением, и поэтому мы возвращаемся. Если больше 0, переходим на предыдущую строку и начинаем поиск оттуда вверх. Мы также проверяем, действителен ли row_index, прежде чем использовать его в get_editor_row.

Давайте теперь разрешим пользователю перейти к следующему совпадению в той же строке:

Если мы хотим, чтобы наш поиск был в той же строке, когда мы должны сохранить row_index как search_index.y_index, когда пользователь нажимает различные клавиши со стрелками. Когда пользователь не перемещается с помощью клавиш со стрелками влево и вправо, мы выполняем поиск так же, как делали это изначально. Если пользователь нажал ArrowRight, то мы выполняем поиск с текущей позиции x и далее, а затем добавляем количество символов перед начальной позицией. Таким образом, мы можем получить общий индекс. Если пользователь нажал ArrowLeft, мы используем .rfind(), чтобы изменить направление поиска. Обратите внимание: если индекс не возвращается, это означает, что в данный момент мы находимся на последнем совпадении в этой строке (в зависимости от направления), поэтому мы просто break вышли из цикла for.

Наконец, давайте не забудем обновить текст подсказки, чтобы пользователь знал, что он может использовать клавиши со стрелками:

В заключительной части мы бы реализовали подсветку синтаксиса 🎨!