Яндекс.Метрика

    cpp

    Разработка кросс-платформенного редактора с подсветкой синтаксиса на основе wxStyledTextCtrl

    Доброе время суток Хабражители!


    Хочу рассказать вам о своем опыте разработки редактора с подсветкой синтаксиса или редактора кода. Речь идет о редакторе для пользовательского языка. Суть этого поста не в детальном описании процесса разработки — я остановлюсь только на наиболее интересных моментах, слабо освященных или вообще пропущенных в документации.
    Scintilla — один из наиболее известных бесплатных компонентов для создания редакторов кода, wxStyledTextCtrl его обертка в библиотеке wxWidgets.
    Scintilla поддерживает большинство используемых языков программирования, поэтому добавление нового лексера зачастую выполняется по аналогии с наиболее близким языком. Более подробно этот процесс освящен тут. Этот способ очень прост в реализации, но имеет один существенный недостаток — придется включить весь код scintilla в свой проект. Для небольших и относительно стабильных проектов это не очень существенно, но для громоздких и активно развивающихся вызывает массу проблем. Поддержка еще сложней если вы используете не сам компонент Scintilla, а его обертку, например, wxStyledTextCtrl. Альтернативой является, так называемая, реализация лексера в контейнере.

    Интеграция лексера в scintilla

    Для начала пару слов о простом способе. В общем случае для создания своего лексера нужно написать всего одну функцию, которая будет отвечать за раскраску измененной части кода:
    static void ColouriseDoc (unsigned int startPos, int length, int initStyle, WordList *keywordlists[], Accessor &styler). Раскрасить или стилизовать указанный диапазон, значит указать стиль для каждого символа. При этом не стоит компонент сам оптимизирует прорисовку текста, а также гарантирует что диапазон [startPos, startPos + length - 1] представляет собой целое количество строк, что облегчает реализацию парсера.

    Реализация лексера в контейнере

    Я начал с создания класса — потомок от wxStyledTextCtrl как указано в примере. Как в и предыдущем способе лексер требует всего одну функцию, которая в данном случае будет вызываться по событию EVT_STC_STYLENEEDED.
    Опыт показал, что если размер редактируемых файлов не превышает 50-60 строк, то можно смело игнорировать передаваемый событием диапазон и стилизовать все содержимое файла, что существенно упрощает лексер. Если же количество строк больше, то обновлять стиль лучше не со строки GetEndStyled(), а с предыдущей строки. Также как и в предыдущем способе событие вызывается для целого числа строк.

    О чем забыли упомянуть в мануале

    Одна из сложностей с которой я столкнулся уже на этом этапе это то что событие вызывается слишком часто. Небольшое исследование поведения компонента в отладчике показало, что после стилизации диапазона [n, m] последним стилизованным символом оказывается вовсе не m. Чаще всего это был предыдущий символ, иногда символ отстоящий от него на 2-3 символа. Причин оказалось несколько:
    • все пользовательские стили должны иметь индексы больше 0,
    • стилизовать нужно абсолютно все, в том числе пробелы и знаки переноса строк
    • стили нужно применять последовательно, т.е. если сначала применить стиль к диапазону [0, 100], а потом к [0, 10] — последним стилизованным окажется 10-й, а не 100-й

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

    SetStyling via SetStyleBytes

    Практически во всех примерах применение стиля указывается как последовательность команд:
    StartStyling(...);
    SetStyling(...);

    в этом случае происходит посимвольное применение стиля, что абсолютно неэффективен и лучше использовать этот, позволяющий передать компоненту заранее подготовленный вектор индексов стилей
    StartStyling(...);
    SetStyleBytes(...);

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

    Русские буквы

    Для того чтобы стили корректно принялись к словам с русские буквами, нужно корректно вычислить индекс последнего символа последовательно выполняя pos = PositionAfter(pos). Аналогичный способ вычисления позиции нужно применять и для получения части слова при вызове автоматической подстановки pos = PositionBefore(pos).

    Скрытие части кода (folding)

    Реализация этой функции не вызывала особых проблем, и пробелов в документации замечено не было. Скажу только что сворачивание основывается на уровнях строк SetFoldLevel(...), которые, я например, расставлял прямо в лексере (именно поэтому удобней построчный парсер).

    Функция автоматической подстановки (autocomplete)

    Несмотря на то что функция относительно подробна рассмотрена в документации, некоторые вещи упущены. В первую очередь оказалось что стандартная функция компонента AutoCompShow(...) не осуществляет фильтрацию по введенной части слова, а только позиционирование. Затем функция AutoCompSelect() которая должна подставлять выбранное пользователем слово оказалась бесполезной, потому что часто вызывала исключение без видимых причин. Вместо нее лучше установить флаг AutoCompSetChooseSingle().

    Подсказки вызова (calltips)

    Другая не менее удобная функция wxStyledTextCtrl это отображение подсказок вызова CallTipShow(). Работа с ней аналогична работе с функцией подстановки AutoCompShow(). Следует учесть, что они прохо сочетаются одновременно.

    С учетом вышеперечисленных особенностей wxStyledTextCtrl позволяет реализовать достаточно сложный лексер в контейнере.