Недавно я всё же решил сесть и разобраться с Yi — текстовым редактором наподобие Vim и Emacs, но написанном на Haskell. В комплекте даже есть Vim и Emacs симуляция.
Из-за отстутствия опыта с Vim или Emacs, мне подошла лишь Cua-симуляция. Хоткеев там мало, но зато они привычные для меня. Поэтому я решил начать с него и написать настройку для себя.
В обычных графических редакторах мне кажется удобным способ использования меню. Нажимаешь alt, открывается меню, где у каждого элемента подчёркнута буква, нажав которую, мы этот элемент выберем.
Таким образом не надо запоминать все команды сразу, а можно начинать пользоваться, подглядывая в меню, постепенно доводя до автоматизма.
Нечто подобное я решил прикрутить и в Yi.
Настраиваем простые хоткеи
Для начала следует разобраться, как же устроен Yi? Проще всего это понять, если посмотреть на уже готовые биндинги, например Cua. Он урезан, и куда примитивнее биндингов-аналогов Vim и Emacs, но для наших целей (написать своё) — самое то.
Первым делом обратим внимание на то, как вообще задаются хоткеи. Это можно видеть по основной функции
keymap :: KeymapSet
keymap = portableKeymap ctrl
-- | Introduce a keymap that is compatible with both windows and osx,
-- by parameterising the event modifier required for commands
portableKeymap :: (Event -> Event) -> KeymapSet
portableKeymap cmd = modelessKeymapSet $ selfInsertKeymap <|> move <|> select <|> rect <|> other cmd
Различные варианты биндингов объединяются при помощи оператора <|>. Посмотрим далее на other cmd:
other cmd = choice [
spec KBS ?>>! deleteSel bdeleteB,
spec KDel ?>>! deleteSel (deleteN 1),
spec KEnter ?>>! replaceSel "n",
cmd (char 'q') ?>>! askQuitEditor,
cmd (char 'f') ?>> isearchKeymap Forward,
cmd (char 'x') ?>>! cut,
cmd (char 'c') ?>>! copy,
cmd (char 'v') ?>>! paste,
cmd (spec KIns) ?>>! copy,
shift (spec KIns) ?>>! paste,
cmd (char 'z') ?>>! undoB,
cmd (char 'y') ?>>! redoB,
cmd (char 's') ?>>! fwriteE,
cmd (char 'o') ?>>! findFile,
cmd (char '/') ?>>! withModeB modeToggleCommentSelection,
cmd (char ']') ?>>! autoIndentB IncreaseOnly,
cmd (char '[') ?>>! autoIndentB DecreaseOnly
]
Как видно, слева комбинация клавиш, справа — действие. Т.е. при нажатии cmd (char 'c') (по умолчанию cmd — ctrl) — получаем copy, код которой тоже незамысловат.
Я скопировал к себе эти определения и решил начать их правку, чтобы соорудить какое-то подобие меню.
Как делать меню?
Чтобы решить, как именно реализовать меню, стоит отправиться в документацию модулей. Всё структурировано достаточно удобно, и в глаза бросается модуль Yi.MiniBuffer. Видимо, это то, что нам надо. Там есть функция
spawnMinibufferE :: String -> KeymapEndo -> EditorM BufferRef
которая принимает выводимый текст и функцию, выставляющую свои биндинги на клавиши. Т.е. то, что нам надо. В строку мы выведем элементы меню, в биндингах отловим выбор элементов меню по клавишам.
Для начала создадим тип, удобный для описания меню. Меню состоит из списка элементов, каждый из которых либо открывает подменю, либо является каким-то действием. Так и запишем:
-- | Menu
type Menu = [MenuItem]
-- | Menu utem
data MenuItem =
MenuAction String (MenuContext -> Char -> Keymap) |
SubMenu String Menu
-- | Menu action context
data MenuContext = MenuContext {
parentBuffer :: BufferRef }
Вариант SubMenu содержит в себе заголовок и подменю, вариант MenuAction — заголовок и функцию, которая создаст нужные биндинги.
MenuContext — это некоторый контекст, который передаётся в действия (пока там только исходный буфер, из которого вызвали меню, это понадобилось для реализации кнопки Save), Char — та кнопка, по нажатию на которую меню необходимо вызвать.
Так как тип рекурсивный, для него можно просто определить свёртку, чтобы потом, пользуясь ей, запускать меню:
-- | Fold menu item
foldItem
:: (String -> (MenuContext -> Char -> Keymap) -> a)
-> (String -> [a] -> a)
-> MenuItem
-> a
foldItem mA sM (MenuAction title act) = mA title act
foldItem mA sM (SubMenu title sm) = sM title (map (foldItem mA sM) sm)
-- | Fold menu
foldMenu
:: (String -> (MenuContext -> Char -> Keymap) -> a)
-> (String -> [a] -> a)
-> Menu
-> [a]
foldMenu mA sM = map (foldItem mA sM)
Также нам понадобятся функции, которые более удобно создадут для нас элементы меню. SubMenu создать просто, SubMenu «File» ..., а вот MenuAction пользоваться сложнее. Поэтому определим несколько функций, которые будут принимать действие (такое же, как справа от ?>>! в биндингах). Я приведу код двух из них:
-- | Action on buffer
actionB_ :: String -> BufferM () -> MenuItem
actionB_ title act = actionB title (const act)
-- | Action on buffer with context
actionB :: String -> (MenuContext -> BufferM ()) -> MenuItem
actionB title act = MenuAction title act' where
act' ctx c = char c ?>>! (do
closeBufferAndWindowE
withGivenBuffer0 (parentBuffer ctx) (act ctx))
Здесь мы создаём MenuItem, который при нажатии на соответствующую кнопку (char c) закроет меню и вызовет действие, которое нам надо.
И последнее, напишем функцию показа меню.
-- | Start menu action
startMenu :: Menu -> EditorM ()
startMenu m = do
-- Получаем контекст, текущий буфер
ctx <- fmap MenuContext (gets currentBuffer)
startMenu' ctx m
where
-- Используя свёртку, преобразуем меню в список пар (заголовок, биндинги)
startMenu' ctx = showMenu . foldMenu onItem onSub where
showMenu :: [(String, Maybe Keymap)] -> EditorM ()
Показать меню — создать минибуфер с элементами через пробел, выставив свои биндинги
showMenu is = void $ spawnMinibufferE menuItems (const (subMap is)) where
menuItems = (intercalate " " (map fst is))
-- Преобразуем простой элемент —
-- пара заголовок + вызываем действие с контекстом, получая биндинги
onItem title act = (title, fmap (act ctx) (menuEvent title)) where
-- Преобразуем вложенное меню —
-- заголовок + создаём биндинг, который по выбору этого элемента покажет подменю
onSub title is = (title, fmap subMenu (menuEvent title)) where
-- нажатие 'c' закрывает минибуфер и открывает новый с подменю
subMenu c = char c ?>>! closeBufferAndWindowE >> showMenu is
-- в каждое меню надо добавить биндинг на Esc, который закроет меню и ничего не выполнит
subMap is = choice $ closeMenu : mapMaybe snd is where
closeMenu = spec KEsc ?>>! closeBufferAndWindowE
Полный код можно посмотреть тут.
Создаём меню
Теперь стоит создать какое-нибудь меню, забиндить на кнопку и начать можно проверять.
Сначала я написал большое развесистое меню, запихнув туда то, что мне попалось при беглых просмотрах различных модулей в Yi. Когда я заметил, что, например, часто захожу в подменю View — Windows, я решил просто вынести это меню на отдельный хоткей.
Теперь можно сплитить окно не только по длинной комбинации V-W-S, но и просто Ctrl-W — S.
Вот код основного меню и подменю Windows:
-- | Main menu
mainMenu :: Menu
mainMenu = [
menu "File" [
actionY_ "Quit" askQuitEditor,
actionY "Save" (fwriteBufferE . parentBuffer)],
menu "Edit" [
actionY_ "Auto complete" wordComplete,
actionE_ "Completion" completeWordB],
menu "Tools" [
menu "Ghci" ghciMenu],
menu "View" [
menu "Windows" windowsMenu,
menu "Tabs" tabsMenu,
menu "Buffers" buffersMenu,
menu "Layout" [
actionE_ "Next" layoutManagersNextE,
actionE_ "Previous" layoutManagersPreviousE]]]
-- | Windows menu
windowsMenu :: Menu
windowsMenu = [
actionE_ "Next" nextWinE,
actionE_ "Previous" prevWinE,
actionE_ "Split" splitE,
actionE_ "sWap" swapWinWithFirstE,
actionE_ "Close" tryCloseE,
actionE_ "cLose-all-but-this" closeOtherE]
Всё меню можно посмотреть тут.
Результат
Прописываем главное меню и подменю на различные комбинации.
Пользуемся!
Итоги
После реализации я прикрутил какой-то встроенный простейший автокомплит, затем интерпретатор GHCi. На очереди тулза hlint (анализирует код и подсказывает, где можно заменить на использование стандартной функции, где написано что-то лишнее и прочее) и прочие.
Весь код доступен на GitHub.
Автор: VoidEx