Настройка редактора Neovim для комфортного кодинга на языке Oberon
Опубликовано 22.01.2025 в OpenBSD
После прошлого моего поста о том, как кодить на Pascal и Oberon под OpenBSD не привлекая внимания санитаров, осталось у меня досадное такое чувство незавершенности какой-то, словно бросил важное дело на полпути. Надо доделать!
Итак, если мне вдруг взбредет в голову плотно кодить на языке Oberon (что, вообще-то, сомнительно), то мне хочется иметь максимально комфортное рабочее окружение для этого. И чего же мне не хватает в этом плане в любимом редакторе Neovim?
Подстветку синтаксиса я уже настроил (точнее, просто воспользовался готовым файлом синтаксиса от добрых людей): https://github.com/kekcleader/vim-oberon
Некоторое неудобство мне, однако, доставлял тот факт, что хотя я и добавил себе в options.lua новый тип файла "oberon", вот так вот:
vim.filetype.add({ extension = {obn = "oberon"} })
файлы с расширением ".Mod" (а равно ".mod"), традиционным для модулей на Oberon, Neovim продолжал определять как "modula-2". И нет, это не чинится простым добавлением еще одного extension - судя по файлу detect.lua, Neovim определяет принадлежность файла к типу "modula-2" отнюдь не только по расширению, а по сложным паттернам, включающим выяснение диалекта Модулы... А в Neovim, насколько я понял, заоверрадйить через расширение можно только то, что через расширение же и определялось. Короче, я плюнул на все эти сложности и запилил вот такой вот костыль, грубо и открыто переопределяющий тип файла при открытии, тупо по расширению, как бы его там ни определили ранее хитрые встроенные детекторы:
-- переопределим обработку расширений для файлов .mod или .Mod
-- чтобы они определялись не как Modula-2, а как Oberon-файлы
-- при событии открытия файла
vim.api.nvim_create_autocmd("BufReadPost", {
pattern = "*",
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
local bufname = vim.api.nvim_buf_get_name(bufnr)
if bufname:match("%.Mod$") or bufname:match("%.mod$") then
vim.api.nvim_buf_set_option(bufnr, 'filetype', 'oberon')
end
end
})
Но это не главная сложность. Главное - что язык Oberon является регистро-зависимым, в том смысле, что ключевые слова полагается писать в верхнем регистре. Тяжкое наследие Алгола-60, ага - столь полюбившаяся массам подсветка синтаксиса в редакторах возникла не так давно, а в печатных изданиях не распространена и поныне, при таком раскладе ключевые слова в верхнем регистре - далеко не худшее решение, наглядное и гармоничное. Вам же в SQL это глаза не режет, а наобoрот, считается хорошим тоном?
Так вот: мне бы хотелось, чтобы ключевые слова и зарезервированные выражения языка Oberon приводились в верхний регистр прямо по ходу ввода, без дополнительных усилий с моей стороны (но только для файлов на Обероне, разумеется, и за исключением комментариев и строковых констант/переменных, где регистр трогать не надо). Верю, что подобные решения, возможно, уже существуют (хотя я сходу не нашел). Ну или что этого легко можно достичь всякими там модными грамматиками для LSP/Treesitter и прочей хипстерской мутоты, но я в это не умею, у меня гуманитарные лапки. А значит что? Правильно: будем говнокодить!
Перво-наперво, запилил я файлик ~/.config/nvim/lua/oberon_upper.lua
, куда вписал тот самый набор ключевых слов Оберона, и сочинил две функции: одна определяет, не находимся ли мы в данный момент в комментарии/строке (по синтаксическим характеристикам файла, для чего нам снова пригодился файл синтаксиса ~/.config/nvim/syntax/oberon.vim
, а вторая функция собственно, определяет, относится ли последнее введенное слово к ключевым и если относится, переводит его в верхний регистр.
-- список ключевых слов, предопределенных наименований типов и процедур в Oberon согласно спецификации языка:
-- (спецификация включает изменения от 2016 года) https://miasap.se/obnc/oberon-report.html
local oberon_keywords = { 'ABS', 'ADR', 'ADS', 'ARRAY', 'ASSERT', 'ASR', 'BEGIN', 'BIT', 'BOOLEAN', 'BY', 'BYTE', 'CASE', 'CHAR', 'CHR', 'COND', 'CONST', 'COPY', 'DEC', 'DIV', 'DO', 'ELSE', 'ELSEIF', 'END', 'EXCL', 'FALSE', 'FLOOR', 'FLT', 'FOR', 'IF', 'GET', 'IMPORT', 'IN', 'INC', 'INCL', 'INTEGER', 'IS', 'LED', 'LEN', 'LSL', 'MOD', 'MODULE', 'NEW', 'NIL', 'ODD', 'OF', 'OR', 'ORD', 'PACK', 'POINTER', 'PROCEDURE', 'PUT', 'REAL', 'RECORD', 'REPEAT', 'RETURN', 'ROR', 'SBC', 'SET', 'SIZE', 'THEN', 'TO', 'TRUE', 'TYPE', 'UNPK', 'UNTIL', 'UML', 'VAR', 'VAL', 'WHILE' }
--функция, проверяющая, находимся ли мы внутри комментария или строки
local function is_inside_comment_or_string()
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
-- для текущей строки нужно вытащить синтаксическую информацию
-- которая определяется согласно ~/,config/nvim/syntax/oberon.vim
local synstack = vim.fn.synstack(row, col + 1)
for _, syn_id in ipairs(synstack) do
local syn_name = vim.fn.synIDattr(syn_id, 'name')
-- в нашем файле синтаксиса oberon.vim наименования такие
if string.match(syn_name, 'OberonComment') or string.match(syn_name, 'OberonString') then
return true
end
end
-- иначе это не коммент/строка
return false
end
-- функция для преобразования последнего введенного слова к верхнему регистру
-- на выход принимает айдишник режима редактора, из которого вызывается:
-- -1 - normal, 1 - insert (не спрашивайте, так повелось и лень переделывать)
function convert_last_word_to_uppercase(mode)
-- сначала проверим, что мы не внутри комментария\строки - если да, то ничего не делаем
if is_inside_comment_or_string() then
return
end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line()
-- Найдем последнее слово перед курсором
local last_word_start = string.find(line:sub(1, col -1), "%S+$")
if (last_word_start == nil) then last_word_start = 1 end
local last_word_end = col -mode
if (last_word_end == -1) then last_word_end = 1 end
local last_word = string.sub(line, last_word_start, last_word_end)
-- Проверяем, является ли слово ключевым словом Oberon
if (last_word ~= nil) then
for k, v in pairs(oberon_keywords) do
if v == string.upper(last_word) then
-- Преобразуем слово в верхний регистр
local new_word = string.upper(last_word)
local new_line = line:sub(1, last_word_start - 1) .. new_word .. line:sub(last_word_end + 1)
vim.api.nvim_set_current_line(new_line)
-- Перемещаем курсор обратно
vim.api.nvim_win_set_cursor(0, { row, col + (#new_word - #last_word) })
end
end
end
end
А чтобы вызывать это великолепие и связанные с ним автокоманды (ну куда тут без них) только при открытии файлов типа "oberon" я создал еще один файлик - ~/.config/nvim/after/filetype/oberon.lua
собственно, определяющий автокоманды и грязные хаки, которые и вызывают упомянутое выше. Вот он:
require('oberon_upper')
-- автокоманды по мере ввода текста в режиме вставки можно реализовать ориентируясь
-- на событие InsertCharPre - оно вызывается ДО вставки символа в буфер, оно знает,
-- какой символ нажат (устанавливает переменную vim.v.char), но, к сожалению, оно не
-- позволит редактировать буфер (textlock) - считается небезопасным редактировать
-- буфер до вставки введенного символа, это может повлечь непредстказуемое поведение
-- редактора... Так что я буду использовать InsertCharPre только чтобы записать введен-
-- ный символ в глобальную переменную last_entered_char
vim.api.nvim_create_autocmd('InsertCharPre', {
callback = function()
last_entered_char = vim.v.char
end
})
-- а здесь настраиваются автокоманды по событию TextChangedI - оно, в отличие от InsertCharPre
-- вызывается ПОСЛЕ вставки символа в буфер, да только вот TextChangedI не знает, какой именно
-- символ был введен.
vim.api.nvim_create_autocmd('TextChangedI', {
callback = function()
-- автоисправление ключевых слов Oberon должно работать только в буферах
-- имеющих тип файла 'oberon" - иначе если мы СНАЧАЛА откроем файл .onb,
-- а потом в соседнем буфере файл другого типа - автозамена будет рабоать
-- и во втором файле тоже, что неудобно
local bufnr = vim.api.nvim_get_current_buf()
local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype')
if ft == 'oberon' then
-- автоисправление будет работать при вводе пробела, точки с запятой, скобки -
-- к сожалению, так нельзя настроить работу по нажатию Enter, поскольку
-- InsetCharPre не возвращает эвента при нажатии Enter, см. костыль ниже
if last_entered_char == ' ' or last_entered_char == ';' or last_entered_char == '(' then
convert_last_word_to_uppercase(1)
end
end
end
})
-- Маппинг для <CR> в режиме вставки
vim.api.nvim_create_autocmd({'BufEnter', 'BufReadPost'}, {
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype')
if ft == 'oberon' then
vim.api.nvim_buf_set_keymap(0, 'i', '<CR>', '<C-o>:lua convert_last_word_to_uppercase(-1)<CR><CR>',
{ noremap = true, silent = true })
end
end
})
И оно работает! По ходу ввода: после нажатия пробела, Enter'а, точки с запятой или скобки переводит ключевые слова в верхний регистр, от чего красиво зажигается подстетка синтаксиса и я чувствую себя счастливее.
Версия первая, сырая, наверняка всплывут какие-то баги и недоработки, но пока что я вполне доволен.