В 4-й части этого пошагового руководства мы преобразовали нашу программу в средство просмотра текста, позволяющее нам прокручивать текстовые файлы с помощью таких клавиш, как PageUp, PageDown, End, Home, ArrowLeft и ArrowRight. В этой части мы разрешили бы пользователю редактировать текст, а также сохранять изменения на диск.

Вставьте обычные символы 🔡

Давайте начнем с написания функции, которая вставляет один символ в строку в заданной позиции:

Сначала мы изменили тип row_content на String, чтобы сделать его изменяемым. Затем мы изменяем сигнатуру функции new. Обратите внимание, что это небольшое изменение не нарушает остальную часть нашей программы 😀. Мы используем String::insert для вставки новой буквы. Если at равно len строки, буква будет добавлена ​​к строке. Наконец, мы вызываем render_row, чтобы render обновлялось.

Напомним, что мы разрешаем пользователю перемещать курсор до 1 строки под файлом. Что, если пользователь решит переместить курсор на эту строку и вставить туда символы? Мы должны создать функцию insert_row, которая сначала добавит новую пустую строку в содержимое файла, затем мы сможем вставлять туда символы.

Мы используем процедурный макрос #[derive] для реализации метода Default для Row. Это значение по умолчанию trait может быть использовано в качестве отката к некоторому значению по умолчанию, значению, которое нас особенно не волнует, что это такое. В этом случае значение по умолчанию создает новый экземпляр Row, где row_content и render являются пустыми строками без распределения памяти.

Если вам нужен туториал по макросу #derive (о чем он и как написать свой собственный), «хлопайте» этому туториалу. Примерно после 100 хлопков я делал туториал по #derive. Вы также можете прокомментировать учебник, который вы хотели бы иметь.

Давайте запустим нашу функцию:

Мы добавили новую функцию get_editor_mut, которая возвращает изменяемую ссылку на Row (в Rust довольно часто встречаются функции с аналогом _mut). Как объяснялось ранее, сначала мы проверяем, находится ли курсор на строке под файлом. Если это так, мы добавляем новую строку. После вставки символа мы увеличиваем cursor_x на 1. В функции process_keypress любой ключ, который еще не сопоставлен с функцией, будет передан в insert_char. Обратите внимание, что мы ограничиваем модификаторы, чтобы что-то вроде Ctrl+ p не вставляло p;

Теперь мы официально обновили нашу программу просмотра текста до текстового редактора. Мы готовы сохранить новый файл.

Сохранить на диск 💾

Теперь давайте добавим функцию save, которая будет обрабатывать все сохранения:

Сначала мы преобразуем row_contents, то есть Vec, в строку. Мы делаем это, сначала создавая итератор элементов row_contents, затем ограничиваем итератор только полем row_content Row, а затем собираем его как Vec<&str>. Затем мы используем функцию join() для создания одной строки, состоящей из подстрок, соединенных \n, то есть новым разделителем строк. (Вам не нужно явно указывать тип как String, как это сделал я. Я просто указал его для ясности).

При открытии файла мы указали write(true) и create(true). create(true) создает новый файл, если файл еще не существует. write(true) делает файл доступным для записи. Обычно для записи в файлы можно было просто использовать std::fs::write(). Но это полностью обрезает файл перед записью содержимого. Усекая файл самостоятельно до той же длины, что и данные, которые мы планируем записать в него, мы делаем всю операцию перезаписи немного более безопасной на случай, если функция set_len завершится успешно, но вызов write() завершится ошибкой. В этом случае файл по-прежнему будет содержать большую часть данных, которые у него были раньше. Но если файл был полностью обрезан, а затем write не удалось, вы потеряете все свои данные.

Также обратите внимание, что нам не нужно «закрывать» файл, как в большинстве языков программирования. Rust автоматически закрывает файл при удалении экземпляра файла.

Все, что нам нужно сделать сейчас, это сопоставить ключ с save(), так что давайте сделаем это! Мы будем использовать Ctrl-S:

Теперь мы уведомим пользователя, сколько данных было записано с помощью set_message(). Также будут отображаться все Ctrl+S к нашему начальному сообщению:

Грязный флаг 🏴

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

Мы называем текстовый буфер «грязным», если он был изменен с момента открытия или сохранения файла. Добавим dirty к Output и инициализируем его 0:

Давайте покажем состояние dirty в строке состояния, отобразив (modified) после имени файла, если файл был изменен.

Теперь увеличим dirty после каждой операции, которая вносит изменения в текст:

Мы могли бы использовать E.dirty = 1 вместо E.dirty++, но увеличивая его, мы можем получить представление о том, «насколько грязен» файл, что может быть полезно.

Теперь мы хотим, чтобы (modified) исчезло при сохранении файла, поэтому давайте просто установим dirty в 0 после метода save:

Теперь вы должны увидеть, как (modified) появляется в строке состояния, когда вы впервые вставляете символ, и вы должны видеть, что он исчезает, когда вы сохраняете файл на диск.

Выйти из подтверждения 🙅‍♂️

Теперь мы готовы предупредить пользователя о несохраненных изменениях, когда он попытается выйти. Если установлено dirty, мы отобразим предупреждение в строке состояния и потребуем от пользователя нажать Ctrl-Q еще три раза, чтобы выйти без сохранения:

Мы используем quit_times для отслеживания количества раз, когда пользователь нажимал Ctrl-Q. Каждый раз, когда они нажимают Ctrl-Q с несохраненными изменениями, мы устанавливаем сообщение о состоянии и уменьшаем quit_times. Когда quit_times достигает 0, мы, наконец, позволяем программе выйти. Когда они нажимают любую клавишу, кроме Ctrl-Q, тогда quit_times сбрасывается обратно на 3 в конце функции process_keypress.

Возврат 🔙

Далее реализуем возврат. Сначала мы создадим функцию delete_char, которая удаляет символ из row_content. Это было бы очень похоже на insert_char:

Теперь давайте создадим аналогичную функцию в Output, которая выполняет различные проверки, подобно тому, как мы реализовали Output.insert_char:

Если курсор зашел за конец файла, то удалять нечего, и сразу return. В противном случае получаем Row на котором стоит курсор, и если слева от курсора есть символ, мы удаляем его и перемещаем курсор на один левее. Мы также увеличиваем dirty в этом случае.

Давайте сопоставим Backspace и Delete с delete_char():

Нажатие клавиши →, а затем Backspace эквивалентно тому, что вы ожидаете от нажатия клавиши Delete в текстовом редакторе: удаляется символ справа от курсора. Вот как мы реализуем ключ Delete выше.

Возврат в начале строки ◀️

В настоящее время delete_char() ничего не делает, когда курсор находится в начале строки. Когда пользователь делает возврат в начале строки, мы хотим добавить содержимое этой строки к предыдущей строке, а затем удалить текущую строку. Таким образом, неявный символ \n между двумя строками заменяется на пробел, чтобы объединить их в одну строку.

Давайте создадим для этого метод join_adjacent_rows():

Сначала мы удаляем следующую строку, а затем присоединяем ее к текущей строке. После этого мы вызываем render_row для обновления поля render в Row.

Теперь мы модифицируем нашу функцию delete_char в Output:

Если курсор стоит в начале первой строки, то делать нечего, поэтому return сразу. В противном случае, если мы находим этот cursor_x == 0, мы вызываем join_adjacent_rows. Мы также устанавливаем cursor_x в конец содержимого предыдущей строки перед добавлением к этой строке. Таким образом, курсор окажется в точке, где две линии соединились. Также обратите внимание, что мы переместили dirty, чтобы оно увеличивалось при удалении символа или при соединении строк.

Обратите внимание, что нажатие клавиши Delete в конце строки работает так, как и ожидал пользователь, соединяя текущую строку со следующей строкой. Это связано с тем, что перемещение курсора вправо в конце строки перемещает его в начало следующей строки. Таким образом, использование клавиши Delete в качестве псевдонима для клавиши →, за которой следует клавиша Backspace, по-прежнему работает.

Входить ¶

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

Мы должны обновить insert_char:

Теперь добавим метод insert_newline, который будет сопоставлен с ключом Enter:

Если мы находимся в начале строки, все, что нам нужно сделать, это вставить новую пустую строку перед строкой, на которой мы находимся.

В противном случае нам придется разделить линию, на которой мы находимся, на две строки. Мы должны обрезать текущую строку, на которой находится курсор, до размера, равного cursor_x. Мы также должны вызвать render_row, чтобы обновить содержимое render. Затем мы вставляем новую строку с содержимым предыдущей строки, начиная с cursor_x.

После добавления новой строки мы увеличиваем cursor_y и устанавливаем cursor_x как 0, чтобы курсор перемещался в начало новой строки. И не забудьте увеличить dirty

Обратите внимание: если вы попытаетесь обрезать current_row после вставки новой строки, программа не скомпилируется. Вы получите cannot borrow `self.editor_rows` as mutable more than once at a time. Это связано с тем, что current_row ссылается на ссылку на Row внутри Vec<Row>, и мы получили эту ссылку, используя cursor_y. Таким образом, вставка новой строки может изменить фактическую позицию current_row на новую позицию, которая не индексируется cursor_y. Например, если после вставки новой строки текущая позиция строки будет скорее cursor_y+1, а не cursor_y, тогда current_row будет указывать на другую ссылку Row, а это не то, что нам нужно. Обходным путем было бы переназначить current_row после вставки, так как это вернет правильную ссылку, которую мы хотим.

Наконец, давайте сопоставим клавишу Enter с insert_newline:

На этом все операции по редактированию текста, которые мы собираемся реализовать, завершены. Если вы хотите, и если вы достаточно смелы, вы можете начать использовать редактор, чтобы изменить его собственный код для остальной части учебника. Если вы это сделаете, вам, вероятно, следует время от времени делать резервные копии (используя git или аналогичный).

Сохранить как ✉️

В настоящее время, когда пользователь запускает нашу программу без аргументов, он получает пустой файл для редактирования, но не может его сохранить. Нам нужен способ предложить пользователю ввести имя файла при сохранении нового файла. Мы хотим, чтобы наш метод prompt мог принимать что-то вроде Save as: {}, а затем заполнять {} вводом, полученным от пользователя. К счастью, макросы могут помочь нам в этом!

Давайте попробуем понять, что делает prompt!(). Он принимает 2 аргумента. Первое — это выражение, для которого мы ограничили тип выражения только Output с помощью строки

let output:&mut Output = &mut $output;

Это заставляет передавать в макрос только экземпляры Output.

Второй аргумент — args, который представляет собой дерево токенов (tt). Дерево токенов относится к одному токену или токенам в соответствующих разделителях (), [] или {}). Тип tt позволяет нашему макросу принимать аргументы формата, подобные println!().

Это позволит нам использовать макрос так же, как мы используем println!(). Оператор * означает, что токены могут повторять любое количество типов.

Ввод пользователя хранится в строке. Затем мы входим в бесконечный цикл, который повторно устанавливает сообщение о состоянии, обновляет экран и ожидает обработки нажатия клавиши. Не то чтобы при установке сообщения о состоянии мы передаем $($arg)* в макрос format!(). Это связано с тем, что макрос format!() также принимает токены. Мы проезжаем $($args)* перед прохождением input. Это приведет к тому, что input будет последним аргументом формата. Таким образом, если что-то вроде Save as {} or {} передается в prompt!(), пользовательский ввод будет вставлен в последний {}.

Когда пользователь нажимает Enter, а его ввод не пуст, сообщение о состоянии очищается и его ввод возвращается. В противном случае, когда они вводят символ, мы добавляем его к input. Затем мы возвращаем None, если не было ввода, или Some(input), если ввод был.

В зависимости от того, где вы определили макрос prompt!(), вам может потребоваться добавить #[macro_export] или #[macro_use], чтобы различные функции могли иметь доступ к макросу. Если вы определили макрос после определения функции, вы должны добавить #[macro_export] или #[macro_use] соответствующим образом.

Теперь давайте запросим у пользователя имя файла до saving, если filename равно None:

Затем давайте позволим пользователю нажать Escape, чтобы отменить приглашение ввода:

Когда подсказка отменяется, мы очищаем input, а затем возвращаем None. Теперь давайте проверим, было ли приглашение None. Если бы это было так, мы бы показали «Сохранить прервано»:

Давайте завершим эту часть, разрешив пользователю нажимать Backspace или Delete в строке ввода:

Мы просто используем .pop, чтобы удалить последний char.

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