Недавно я разработал ещё один режим GNU Emacs для C-подобного языка программирования C2. Если в предыдущий раз для другого C-подобного языка я написал код с нуля, то в этот раз решил воспользоваться возможностью так называемого наследования режимов. В этой статье хочу поделиться с вами как это делается, и что у меня из этого вышло. (Предполагается, что читатель ознакомился с материалом предыдущей статьи Как написать свой режим для GNU Emacs и опубликовать его в MELPA или имеет собственный уникальный опыт разработки режимов GNU Emacs.)
Введение
Как рассказывалось в предыдущей статье, я уже пытался использовать наследование режимов для C-подобного языка. Однако в тот раз из этого ничего хорошего не вышло. Основная причина была в том, что тот язык не использовал символ точки с запятой в качестве признака завершения некоторых конструкций, что приводило к неверной расстановке отступов при переходе на новую строку. Тогда мне пришлось сделать полное описание синтаксиса языка и написать ту самую функцию, обеспечивавшую правильность отступов.
Язык C2 позиционируется как эволюция языка C. Наиболее заметным отличием является отказ от препроцессора с его #include и #define. Есть и другие новшества, за которыми отсылаю читателя к документации. Также рекомендую обратиться к примерам кода и тестам в коде его компилятора.
Язык C2 сохранил обязательность точки с запятой, что вселяло надежду на быструю реализацию режима GNU Emacs. Однако, оказалось, что следующая простая конструкция работает, но не так как надо:
;;;###autoload (define-derived-mode c2-mode c-mode "C2" "Major mode for editing code written in the C2 Programming Language.")
Проблема была в том, что в этом случае файлы C2 воспринимаются как файлы C! Соответствено и обрабатываются они как файлы C, и все режимы настроенные для C будут запускаться для C2. А значит мы увидим море проблем и ложных сообщений об ошибках в коде на C2. Непорядок!
Тогда я решил ознакомиться с кодом режимов для других C-подобных языков, чтобы разобраться, а как они были реализованы? Вот эти языки и их режимы: D, Dart, C#, Go, JavaScript, Kotlin, Rust. Смотрел также в код встроенных режимов для C++, Java, Perl. И оказалось всё очень неоднозначно! Во-первых, лишь часть режимов использовали это самое наследование от C. Во-вторых, это наследование — ну такое…
В общем, пришлось мне изрядно попотеть. Ещё одну проблему подбрасывал сам C2. Оказалось, что для него нет грамматики! А значит за описанием его конструкций нужно идти в документацию. Анализ примеров из кода компилятора навёл на мысль, что между документацией и тестами есть расхождение… Ну да ладно.
Структура кода режима
Рассказ будет вестись с примерами кода из написанного мною режима.
В самом начале кода режима производим загрузку необходимых модулей:
(require 'cc-bytecomp) (require 'cc-fonts) (require 'cc-langs) (require 'cc-mode) (require 'compile) (eval-when-compile (let ((load-path (if (and (boundp 'byte-compile-dest-file) (stringp byte-compile-dest-file)) (cons (file-name-directory byte-compile-dest-file) load-path) load-path))) (load "cc-mode" nil t) (load "cc-fonts" nil t) (load "cc-langs" nil t) (load "cc-bytecomp" nil t)))
И объявляем наш режим как наследник режима c-mode:
(eval-and-compile (c-add-language 'c2-mode 'c-mode))
На время пропустим последующий код и перейдём в его конец. Как обычно, основная часть кода режима заключается в следующих строках:
;;;###autoload (define-derived-mode c2-mode prog-mode "C2" "Major mode for editing code written in the C2 Programming Language. Key bindings: \\{c2-mode-map}" :after-hook (c-update-modeline) (c-initialize-cc-mode t) (use-local-map c2-mode-map) (c-init-language-vars c2-mode) (c-common-init 'c2-mode) (c-run-mode-hooks 'c-mode-common-hook) (setq-local comment-start "// " comment-end "" block-comment-start "/*" block-comment-end "*/")) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.\\(c2\\|c2i\\|c2t\\)\\'" . c2-mode)) (provide 'c2-mode)
Здесь в define-derived-mode создаётся новый режим, именуемый в коде c2-mode, наследуемый от prog-mode, имеющий название C2, которое будет отображаться в mode line, и строка документации со включением описания привязок к клавишам, которое описывается в переменной c2-mode-map:
(defvar c2-mode-map () "Keymap for C2 mode buffers.") (if c2-mode-map nil (setq c2-mode-map (c-make-inherited-keymap)))
Я не стал добавлять никаких дополнительных комбинацией клавиш, а просто наследую их вызовом c-make-inherited-keymap.
Вернёмся к define-derived-mode и продолжим разбирать код построчно.
:after-hook (c-update-modeline)
c-update-modeline будет вызвано после срабатывания всех hook режима. Как я понял из кода, эта недокументированная функция настраивает mode line для нашего режима.
Вызов (c-initialize-cc-mode t) по-сути осуществляет то самое наследование от cc-mode, базового режима для C-подобных языков. Эта функция инициализирует cc-mode для текущего буфера. Опциональный параметр указывает, что нам нужна только базовая инициализация, а остальное мы сделаем сами.
Вызов (use-local-map c2-mode-map) задаёт для нашего режима клавиатурные комбинации, описанные раннее в переменной c2-mode-map.
Макрос (c-init-language-vars c2-mode) инициализирует для нашего режима все необходимые переменные.
Вызов (c-common-init 'c2-mode) выполняет инициализацию дополнительных возможностей для нашего режима. Внутри себя вызывает c-basic-common-init, выполняющую базовую инициализацию режима.
Вызов (c-run-mode-hooks 'c-mode-common-hook) вызывает общий hook для cc-mode.
В коде ниже настраивается подсветка комментариев:
(setq-local comment-start "// " comment-end "" block-comment-start "/*" block-comment-end "*/")
Но, как и следующий код, это не дало должного эффекта. Я так и не смог заставить cc-mode обрабатывать комментарии как комментарии. Хотя подобный код присутствовал в референсных режимах.
(c-lang-defconst c-block-comment-starter c2 "/*") (c-lang-defconst c-block-comment-ender c2 "*/") (c-lang-defconst c-comment-start-regexp c2 "/[*+/]") (c-lang-defconst c-block-comment-start-regexp c2 "/[*+]") (c-lang-defconst c-line-comment-starter c2 "//")
В следующей строке, как обычно, происходит связывание режима с определёнными расширениями имён файлов:
;;;###autoload (add-to-list 'auto-mode-alist '("\\.\\(c2\\|c2i\\|c2t\\)\\'" . c2-mode))
Наконец, экспортируем наш режим:
(provide 'c2-mode)
Декларация синтаксических элементов
Вернёмся к пропущенному коду.
(defvar c-syntactic-element)
c-syntactic-element хранит синтаксические элементы и как-то используется внутри cc-mode.
(declare-function c-populate-syntax-table "cc-langs.el" (table))
c-populate-syntax-table содержит код обработки синтаксиса C-подобных языков, в частности для комментариев (!). Спрашивается, а что ж оно тогда не работает-то?
Далее идёт серия использования макроса c-lang-defconst, с помощью которого декларируются ключевые слова языка и прочие синтаксические элементы, такие как операторы.
(c-lang-defconst c-identifier-ops c2 '((left-assoc ".")))
Оператор точка объявляется левоассоциативным.
Дальше идёт описание всех ключевых слов для правильной интерпретации и подсветки. Не буду подробно комментировать, здесь довольно понятно, хотя и не всегда очевидно.
(c-lang-defconst c-ref-list-kwds c2 '("import" "local" "module")) (c-lang-defconst c-protection-kwds c2 '("public")) (c-lang-defconst c-constant-kwds c2 '("false" "true" "nil")) (c-lang-defconst c-primitive-type-kwds c2 '("bool" "char" "f32" "f64" "i8" "i16" "i32" "i64" "isize" "u8" "u16" "u32" "u64" "usize" "void")) (c-lang-defconst c-decl-start-kwds c2 '("fn" "module" "type")) (c-lang-defconst c-type-prefix-kwds c2 '("enum" "struct" "union")) (c-lang-defconst c-class-decl-kwds c2 '("enum" "struct" "union")) (c-lang-defconst c-typedef-decl-kwds c2 '("type")) (c-lang-defconst c-brace-list-decl-kwds c2 '("enum")) (c-lang-defconst c-modifier-kwds c2 '("const" "local" "volatile")) (c-lang-defconst c-block-stmt-1-kwds c2 '("as" "case" "do" "else")) (c-lang-defconst c-block-stmt-2-kwds c2 '("for" "if" "sswitch" "switch" "while")) (c-lang-defconst c-simple-stmt-kwds c2 '("assert" "break" "continue" "default" "elemsof" "fallthrough" "return" "sizeof" "static_assert")) (c-lang-defconst c-paren-type-kwds c2 '("cast")) (c-lang-defconst c-decl-hangon-kwds c2 '("as" "local")) (c-lang-defconst c-paren-nontype-kwds c2 '("assert" "elemsof" "sizeof" "static_assert")) (c-lang-defconst c-paren-stmt-kwds c2 '("for" "if" "sswitch" "swtich" "while")) (c-lang-defconst c-label-kwds c2 '("case" "default")) (c-lang-defconst c-before-label-kwds c2 '("goto")) (c-lang-defconst c-return-kwds c2 '("return"))
Перечисляем операторы присваивания:
(c-lang-defconst c-assignment-operators c2 '("=" "*=" "/=" "%=" "+=" "-=" "<<=" ">>=" "&=" "^=" "|="))
Описываем операторы (здесь тоже всё довольно понятно):
(c-lang-defconst c-operators c2 `( (left-assoc ".") (postfix "(" ")" "[" "]" "++" "--") (prefix "!" "-" "~" "*" "&" "++" "--") (infix "." "*" "/" "%" "<<" ">>" "^" "|" "&" "+" "-") (infix "==" "!=" ">=" "<=" ">" "<" "&&" "||") (ternary "?:") (infix "=" "*=" "/=" "%=" "+=" "-=" "<<=" ">>=" "&=" "^=" "|=") (infix ",")))
Описываем операторы-скобки и звёздочку:
(c-lang-defconst c-opt-type-suffix-key c2 (concat "\\(\\[" (c-lang-const c-simple-ws) "*\\]\\|\\*\\)"))
Описываем уровни подсветки, чтобы управлять скоростью и качеством:
(defconst c2-font-lock-keywords-1 (c-lang-const c-matchers-1 c2) "Minimal highlighting for C2 mode.") (defconst c2-font-lock-keywords-2 (c-lang-const c-matchers-2 c2) "Fast normal highlighting for C2 mode.") (defconst c2-font-lock-keywords-3 (c-lang-const c-matchers-3 c2) "Accurate normal highlighting for C2 mode.") (defvar c2-font-lock-keywords c2-font-lock-keywords-3 "Default expressions to highlight in C2 mode.")
Фух! Код режима закончился. Работоспособность режима можно оценить по обложке к статье.
Заключение
Лёгкой прогулки не вышло. Как обычно сложность кода перенесли из одного места в другое. Не знаю, стоит ли использовать этот подход для очередного C-подобного языка или нет. Вот разработчики rust-mode написали весь код с нуля. А ещё есть SMIE и Tree Sitter. Так что не прощаемся, я думаю…
(c) Симоненко Евгений, 2025
ссылка на оригинал статьи https://habr.com/ru/articles/911648/
Добавить комментарий