Эта часть завершает серию Создайте свой текстовый редактор с помощью Rust. Для тех, кто, возможно, наткнулся на эту страницу, не просмотрев другие части, вот краткое изложение того, что мы сделали до сих пор: Мы начали с «Настройка», а затем перешли к чтение нажатия клавиш и вход в «сырой режим», к рисованию на экране и перемещению курсора, к отображению текстовых файлов (делаем нашу программу текстовым представлением ), редактирование текстовых файлов и сохранение изменений, реализация классной функции поиска и, наконец, в эту часть, где мы добавляем подсветка синтаксиса.

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

Начнем с окрашивания каждой цифры в голубой цвет:

Мы больше не добавляем текст, который хотим отображать, прямо в editor_contents. Вместо этого мы перебираем все символы, а затем проверяем, является ли текущий символ цифрой. Если это так, мы добавляем escape-последовательность для голубого цвета, добавляем символ и, наконец, сбрасываем цвет (вы можете найти, чему соответствует каждая escape-последовательность с точки зрения кодов ANSI, в википедии). Цвета, которые мы будем использовать, поддерживаются многими терминалами. Если терминал не поддерживает цвета, то просто добавляем цифру и игнорируем ошибку; Синтаксис let _ = ... просто не позволяет компилятору сообщать о необработанных ошибках.

Подсветка синтаксиса рефакторинга ✨

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

Правила выделения могут отличаться от одного языка программирования к другому. Например, то, как мы можем захотеть выделить синтаксис, скажем, в файле Python, может отличаться от того, как мы хотели бы выделить его, если бы это был файл Rust. Но что хотелось бы сделать, так это написать «общий» метод для подсветки синтаксиса, который можно было бы легко модифицировать или специализировать для разных языков программирования. Итак, мы собираемся создать trait (также известный как интерфейс в большинстве объектно-ориентированных языков программирования, таких как Java и Kotlin), который поможет нам это реализовать.

Но даже перед созданием trait давайте добавим новое поле highlight в Row, которое будет содержать информацию о подсветке для этого Row.

Теперь добавим trait.

На данный момент у трейта есть метод, который просматривает символы Row и выделяет их, устанавливая каждое значение в массиве highlight. Давайте напишем одну реализацию SyntaxHighlight:

Код заворачивается в macro, прежде чем мы реализуем SyntaxHighlight. Этот вид обеспечивает #[derive] для этой структуры, просто мы используем macro_rules, так что это менее сложно. Макрос имеет один параметр, и это имя struct, которое вы хотите создать. Затем макрос создает структуру и получает для нее SyntaxHighlight.

В методе update_syntax мы получаем Row, которое нужно обновить, а затем перебираем chars этого Row, а затем обновляем Row.highlight. Мы добавляем простой assert_eq!(), чтобы убедиться, что наш код работает должным образом. Вы можете удалить его после того, как убедитесь, что программа работает нормально.

Затем давайте создадим функцию syntax_color(), которая сопоставляет значения в highlight с фактическими цветовыми кодами, с помощью которых мы хотим их отрисовать:

Обратите внимание, что Color происходит от crossterm::style::Colors.

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

Функция color_row такая же, как и в draw_rows, но немного изменена, чтобы использовать вместо нее методы trait. Теперь добавим SyntaxHighlight к Output:

Поскольку мы используем динамическую отправку, мы используем синтаксис Box<dyn >. Не для каждого типа файла может быть реализован метод подсветки синтаксиса, поэтому мы заключаем это поле в Option<T>. Мы используем RustHighlight struct, которые мы создали из макроса, в качестве значения по умолчанию на данный момент. В draw_rows мы заменяем код окраски на syntax_highlight, если он доступен.

Чтобы код заработал, добавим update_syntax везде, где изменяется Row, аналогично тому, как мы реализовали render_row():

Теперь всякий раз, когда происходит изменение в любой из строк, будет вызываться update_syntax, чтобы отразить изменения. На этом мы завершаем рефакторинг системы подсветки синтаксиса.

Красочные результаты поиска 🔭

Прежде чем мы начнем выделять строки, ключевые слова и все такое, давайте воспользуемся нашей системой выделения, чтобы выделить результаты поиска. Мы начнем с добавления SearchMatch к HighlightType enum и сопоставления его с синим цветом:

Теперь все, что нам нужно сделать, это установить совпадающую подстроку с highlight на SearchMatch в нашем коде поиска:

Во-первых, мы получаем изменяемую ссылку на Row с query. Чтобы выделить совпадающее слово, мы просто перебираем массив highlight, начиная с длины index до index + query, устанавливая соответствующее значение highlight равным SearchMatch.

Восстановить подсветку синтаксиса после поиска ⚖️

В настоящее время результаты поиска остаются выделенными синим цветом даже после того, как пользователь завершил использование функции поиска. Мы хотим восстановить значения highlight до их предыдущего значения после каждого поиска. Мы собираемся получить Clone для HighlightType, аналогично тому, что мы сделали для CursorController. Затем мы сохраним исходный highlight и соответствующий row_index в SearchIndex:

Теперь давайте изменим find_callback:

В начале функции мы используем .take(), чтобы вернуть собственную версию значения в previous_highlight, если таковая имеется. take() заменяет исходное значение на None, что нам и нужно в данном случае. Если было предыдущее совпадение, мы сбрасываем highlight этой строки. Когда есть совпадение, мы сохраняем предыдущий highlight перед модификацией.

Задание для читателя:

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

Оптимизировать color_row() ⚡️

Пока раскраска работает нормально, но color_row выполняет 3 операции, которые могут не понадобиться. Сначала он записывает управляющую последовательность для цвета, затем записывает одну букву и, наконец, записывает управляющую последовательность для сброса цвета. На практике большинство символов будет того же цвета, что и предыдущий символ, поэтому большинство escape-последовательностей избыточны. Давайте отслеживать текущий цвет текста по мере того, как мы перебираем символы, и распечатывать escape-последовательность только при изменении цвета:

После цикла по chars мы сбрасываем цвет, чтобы оставшиеся строки не затрагивались.

Красочные цифры 🖍

Хорошо, давайте начнем работать над правильным выделением чисел. Во-первых, мы изменим наш цикл for в update_syntax на цикл while, чтобы позволить нам использовать несколько символов на каждой итерации:

Теперь давайте определим функцию is_separator(), которая принимает символ и возвращает true, если он считается символом-разделителем:

Так как многие языки программирования имеют похожие разделители, мы делаем is_separator методом по умолчанию.

Прямо сейчас числа выделяются, даже если они являются частью идентификатора, например 32 в int32_t. Чтобы исправить это, нам потребуется, чтобы числам предшествовал символ-разделитель, который включает пробелы или знаки препинания. Давайте добавим к update_syntax() переменную previous_separator, которая отслеживает, был ли предыдущий символ разделителем. Затем давайте используем его, чтобы правильно распознавать и выделять числа:

Мы инициализируем previous_separator значением true, потому что считаем начало строки разделителем. (Иначе числа в самом начале строки не выделялись бы.)

previous_highlight устанавливается на тип подсветки предыдущего символа. Чтобы выделить цифру, мы теперь требуем, чтобы предыдущий символ был либо разделителем, либо также выделялся с помощью HightlightType::NUMBER.

Когда мы решаем выделить текущий символ определенным образом, мы увеличиваем i, чтобы «использовать» этот символ, устанавливаем previous_separator на false, чтобы указать, что мы находимся в процессе выделения чего-то, а затем continue цикл. Мы будем использовать этот шаблон для каждой вещи, которую мы выделяем.

Если мы оказываемся в нижней части цикла while, мы устанавливаем previous_separator в зависимости от того, является ли текущий символ разделителем, и увеличиваем i, чтобы использовать символ.

Теперь давайте поддержим подсветку чисел, содержащих десятичные точки:

Символ ., следующий за символом, который мы только что выделили как число, теперь будет считаться частью числа.

Определить тип файла 🧑‍🔬

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

Для начала мы добавим extensions() к SyntaxHighlight, чтобы все типы, реализующие SyntaxHighlight, должны были указывать соответствующие расширения файлов:

После создания функции extensions мы реализуем ее в нашем макросе. Сначала мы создаем новый параметр extensions, который также принимает выражение (expr), и назовем его ext. Затем мы создаем struct с полем с именем extensions типа &’static [&’static str] (мы должны явно указать тип ‘static, так как мы используем его в поле struct). Мы также создаем метод new для возврата экземпляра struct с указанными расширениями.

Теперь давайте создадим метод select_syntax, который будет возвращать объект SyntaxHighlight или None, если для этого расширения файла нет соответствующего SyntaxHighlight:

select_syntax теперь возвращает правильный объект SyntaxHighlight. Чтобы добавить новый объект SyntaxHighlight, просто вставьте его в массив. Поскольку EditorRows — это struct с именем файла, мы передаем изменяемый syntax_highlight в EditorRows::new(), чтобы он был изменен для возврата правильного объекта SyntaxHighlight.

Не забудем обновить syntax_highlight, когда пользователь использует SaveAs:

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

Далее мы покажем текущий тип файла. Некоторые языки программирования, такие как c, могут иметь файлы с разными расширениями (например, .h и .c), поэтому мы не можем использовать расширение для типа файла. Мы хотим, чтобы тип файла просто показывал, к какому языку программирования принадлежит файл. Для этого добавим в SyntaxHighlight новый метод, который возвращает соответствующий тип файла:

Теперь покажем тип файла, если для него реализован SyntaxHighlight:

Если для этого типа файла нет соответствующего SyntaxHighlight, мы показываем «no ft» (нет типа файла).

Красочные струны 📝

Теперь давайте начнем правильно выделять. Начнем с выделения строк:

Мы окрашиваем струны в зеленый цвет. В некоторых языках программирования (таких как Rust) одинарная кавычка относится к символьному литералу, в то время как в других, таких как python, она относится к строке. А пока мы раскрасим «символьный литерал» в темно-зеленый цвет. (Позже вы можете использовать логическое значение или подобное ему, чтобы указать, следует ли делать это различие.) Мы будем использовать переменную in_string, чтобы отслеживать, находимся ли мы в данный момент внутри строки. Если да, то мы будем выделять текущий символ как строку, пока не нажмем закрывающую кавычку:

На самом деле мы сохраняем либо символ двойной кавычки ("), либо символ одинарной кавычки (') в качестве значения in_string, чтобы знать, какой из них закрывает строку.

Итак, просматривая код сверху вниз: если установлено in_string, то мы знаем, что текущий символ может быть выделен с помощью HightlightType::String. Затем мы проверяем, является ли текущий символ закрывающей кавычкой (val == c), и если да, то сбрасываем in_string на None. Затем, поскольку мы выделили текущий символ, мы должны использовать его, увеличивая i и продолжая итерацию текущего цикла. Мы также устанавливаем previous_separator на true, чтобы, если мы закончили выделять строку, закрывающая кавычка считалась разделителем.

Если мы в данный момент не находимся в строке, то мы должны проверить, находимся ли мы в начале строки, проверив наличие двойной или одинарной кавычки. Если это так, мы сохраняем цитату в in_string, выделяем ее с помощью HightlightType::String и используем.

Обычно, когда в строке встречается последовательность \' или \", экранированная кавычка не закрывает строку в подавляющем большинстве языков. Например, в строке:

else if c == '"' || c == '\'' {

Если мы находимся в строке и текущий символ — обратная косая черта (\), и в этой строке есть по крайней мере еще один символ, следующий за обратной косой чертой, тогда мы выделяем символ, который идет после обратную косую черту с HightlightType::String и использовать его. Мы увеличиваем i на 2, чтобы использовать оба символа одновременно.

Красочные однострочные комментарии 📎

Далее давайте выделим однострочные комментарии. Выделим комментарии темно-серым цветом:

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

Теперь к коду подсветки:

Возможно, в качестве comment_start была передана пустая строка, поэтому в этом случае мы не будем выделять комментарии. Мы заключаем наш код подсветки комментариев в оператор if, который гарантирует, что мы не находимся в строке, поскольку мы размещаем этот код над кодом подсветки строки (порядок имеет значение в этой функции). Мы используем метод .as_bytes, так как render тоже возвращает байты этой строки.

Затем мы проверяем, является ли этот символ началом комментария. Если это так, мы присваиваем остальной части строки настройку HighlightType::Comment, а затем разрываем цикл; Диапазон i..i+ comment_start.len() охватывает chars, что является точным len из comment_start. Но так как i + comment_start.len() может переполниться, мы используем cmp::min, чтобы предотвратить это.

Красочные ключевые слова 🔑

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

Что мы просто делаем в макросе, так это добавляем новое поле с именем keywords, которое может содержать произвольное количество элементов, так что каждый элемент должен сначала иметь указанный цвет, за которым следует ;, а затем ключевые слова, которые мы хотим покрасить в этот цвет. указано. Мы также добавили & в качестве разделителя, чтобы такие строки, как &str и &mut self, и подписи отображались правильно. (Также добавлено [])

Обратите внимание, как с помощью макроса мы можем написать собственный синтаксис для построения нашей структуры.

Давайте изменим наше перечисление HighlightType, чтобы мы могли напрямую передавать цвет:

Теперь выделим их:

Ключевые слова требуют разделителя как до, так и после ключевого слова. В противном случае match в rematch, matching или matched будет выделено как ключевое слово, чего мы определенно хотим избежать. Поэтому мы проверяем previous_separator, чтобы убедиться, что перед ключевым словом стоит разделитель, прежде чем перебирать все возможные ключевые слова. Напомним, что keywords может содержать любое количество цветов, произвольное количество пар ключевых слов. Итак, мы расширяем макрос ($( )*), чтобы получить каждый цвет, произвольное количество пары ключевых слов. Затем мы снова расширяем макрос, чтобы теперь мы могли работать с каждым ключевым словом. Обратите внимание, что цвет по-прежнему доступен в этой области.

Подобно тому, как мы реализовали комментирование, для каждого ключевого слова мы определяем, вызовет ли его индексация в render переполнение. Мы делаем эту проверку, прежде чем сравнивать, является ли этот символ началом ключевого слова. Мы также проверяем, является ли символ после ключевого слова разделителем. Если ключевое слово является последним словом в строке, мы рассматриваем его как ключевое слово. Затем мы выделяем все ключевое слово. После выделения увеличиваем i на длину ключевого слова, устанавливаем previous_separator и затем continue цикл.

Красочные многострочные комментарии 🖇

Осталось реализовать еще одну функцию: выделение многострочных комментариев. Мы окрасим многострочные комментарии тем же цветом, что и однострочные комментарии. Мы позволим каждому языку указать начало и конец многострочного комментария. В Rust это /* и */ соответственно.

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

Теперь о коде подсветки. Мы пока не будем беспокоиться о нескольких строках.

Сначала мы добавляем логическую переменную in_comment, чтобы отслеживать, находимся ли мы в данный момент внутри многострочного комментария. Переходя к циклу while, мы проверяем, не находимся ли мы в строке, потому что наличие /* внутри строки не приводит к началу комментария практически на всех языках. Если мы в данный момент находимся в многострочном комментарии, то мы можем безопасно выделить текущий символ. Затем мы проверяем, находимся ли мы в конце комментария. Если да, то мы выделяем оставшуюся строку, указывающую на конец комментария. Если мы не в конце комментария, мы просто используем текущий символ, который мы уже выделили.

Если мы в данный момент не находимся в многострочном комментарии, то мы проверяем, находимся ли мы в начале многострочного комментария. Если это так, мы выделяем всю строку начала многострочного комментария, устанавливаем in_comment в значение true, а затем соответствующим образом увеличиваем i.

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

Теперь давайте поработаем над выделением многострочных комментариев, которые на самом деле занимают несколько строк. Для этого нам нужно знать, является ли предыдущая строка частью незакрытого многострочного комментария. Давайте добавим логическую переменную is_comment к Row struct:

Теперь последний шаг:

Теперь мы назначаем in_comment перед current_row, поскольку in_comment заимствует editor_rows неизменяемую, а current_row заимствует ее изменяемую, и мы не можем заимствовать изменяемую переменную как неизменяемую, пока она используется. Мы инициализируем in_comment значением true, если в предыдущей строке есть незакрытый многострочный комментарий. В этом случае текущая строка будет выделена как многострочный комментарий.

В нижней части update_syntax() мы устанавливаем значение is_comment текущей строки в любое состояние, в котором осталось in_comment после обработки всей строки. Это говорит нам, закончилась ли строка незакрытым многострочным комментарием или нет. Затем мы должны подумать об обновлении синтаксиса следующих строк в файле. До сих пор мы обновляли синтаксис строки только тогда, когда пользователь меняет эту конкретную строку. Но с многострочными комментариями пользователь может закомментировать весь файл, просто изменив одну строку. Итак, похоже, нам нужно обновить синтаксис всех строк, следующих за текущей строкой. Однако мы знаем, что выделение следующей строки не изменится, если не изменится значение is_comment этой строки. Поэтому мы проверяем, изменилось ли оно, и вызываем update_syntax() в следующей строке только в том случае, если is_comment изменилось (и если в файле есть следующая строка). Поскольку update_syntax продолжает вызывать себя со следующей строкой, изменение будет продолжать распространяться на все больше и больше строк, пока одна из них не останется неизменной, и в этот момент мы знаем, что все строки после этой также должны быть неизменными.

Это оно! 🎊🎉

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

Я также выпущу еще несколько подобных руководств, поэтому, если вы еще не подписались, вам, вероятно, следует!

Вы можете добавить некоторые дополнительные функции, которые вы хотели бы. Могу я предложить некоторые.

Идеи для функций, которые можно реализовать самостоятельно

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

Вот несколько идей, которые вы могли бы попробовать, примерно в порядке возрастания сложности:

  • Дополнительные типы файлов. Добавьте дополнительные правила синтаксиса для различных языков программирования. Вы можете просто сделать это с помощью макроса syntax_struct! и не забудьте включить его Output::select_syntax().
  • Номера строк.Отображение номера строки слева от каждой строки файла.
  • Мягкий отступ: Если вам нравится использовать пробелы вместо табуляции, сделайте так, чтобы клавиша Tab вставляла пробелы вместо \t.
  • Автоматический отступ. При начале новой строки делайте отступ на том же уровне, что и у предыдущей строки.
  • Мягкий перенос строк. Если длина строки превышает ширину экрана, используйте несколько строк на экране для ее отображения вместо горизонтальной прокрутки.
  • Копировать и вставлять. Предоставьте пользователю возможность выделить текст, а затем скопировать выделенный текст, нажав Ctrl-C, и позволить вставить скопированный текст, нажав Ctrl-V.
  • Управление мышью. Позвольте пользователю прокручивать файл с помощью мыши. Вы также можете использовать это, чтобы пользователь не прокручивал файл при прокрутке вниз. Вы также можете разрешить пользователю вставлять символы там, где произошел последний щелчок левой кнопкой мыши. Вы можете найти фору здесь.
  • Больше раскраски.Другие выражения, такие как имена функций, различные "типы", такие как String, Option и т. д., наряду с macro_rules и различными макросами, могут использовать некоторую подсветку. В настоящее время время жизни не выделено должным образом, поэтому вы также можете исправить это.
  • Файл конфигурации: Попросите редактора прочитать файл конфигурации (возможно, с именем .{opened_file}.pc), чтобы установить параметры, которые в настоящее время являются постоянными, например TAB_STOP и QUIT_TIMES. Попробуйте сделать больше настроек.
  • Несколько буферов: разрешить одновременное открытие нескольких файлов и возможность переключения между ними.
  • UTF-8: Может показаться, что это не такая уж большая проблема, но, как говорится в книге Rust, строки не так уж и просты. В настоящее время недопустимые символы UTF-8 приводят к сбою редактора. Кроме того, символы размером более 1 байта (например, смайлики) могут привести к сбою редактора при прокрутке этих символов. Вы могли бы использовать итераторы для таких как chars(). skip(). take() вместо прямой индексации (что вызывает панику). Это также включает в себя изменение функций редактирования программы для вставки и удаления символов без паники, а также фиксацию положения курсора при наличии этих многобайтовых символов.