Недавно я всё же решил сесть и разобраться с Yi — текстовым редактором наподобие Vim и Emacs, но написанном на Haskell. В комплекте даже есть Vim и Emacs симуляция.
Из-за отстутствия опыта с Vim или Emacs, мне подошла лишь Cua-симуляция. Хоткеев там мало, но зато они привычные для меня. Поэтому я решил начать с него и написать настройку для себя.
В обычных графических редакторах мне кажется удобным способ использования меню. Нажимаешь alt, открывается меню, где у каждого элемента подчёркнута буква, нажав которую, мы этот элемент выберем.
Таким образом не надо запоминать все команды сразу, а можно начинать пользоваться, подглядывая в меню, постепенно доводя до автоматизма.
Нечто подобное я решил прикрутить и в Yi.

Для начала следует разобраться, как же устроен Yi? Проще всего это понять, если посмотреть на уже готовые биндинги, например Cua. Он урезан, и куда примитивнее биндингов-аналогов Vim и Emacs, но для наших целей (написать своё) — самое то.
Первым делом обратим внимание на то, как вообще задаются хоткеи. Это можно видеть по основной функции
Различные варианты биндингов объединяются при помощи оператора <|>. Посмотрим далее на other cmd:
Как видно, слева комбинация клавиш, справа — действие. Т.е. при нажатии cmd (char 'c') (по умолчанию cmd — ctrl) — получаем copy, код которой тоже незамысловат.
Я скопировал к себе эти определения и решил начать их правку, чтобы соорудить какое-то подобие меню.
Чтобы решить, как именно реализовать меню, стоит отправиться в документацию модулей. Всё структурировано достаточно удобно, и в глаза бросается модуль Yi.MiniBuffer. Видимо, это то, что нам надо. Там есть функция
которая принимает выводимый текст и функцию, выставляющую свои биндинги на клавиши. Т.е. то, что нам надо. В строку мы выведем элементы меню, в биндингах отловим выбор элементов меню по клавишам.
Для начала создадим тип, удобный для описания меню. Меню состоит из списка элементов, каждый из которых либо открывает подменю, либо является каким-то действием. Так и запишем:
Вариант SubMenu содержит в себе заголовок и подменю, вариант MenuAction — заголовок и функцию, которая создаст нужные биндинги.
MenuContext — это некоторый контекст, который передаётся в действия (пока там только исходный буфер, из которого вызвали меню, это понадобилось для реализации кнопки Save), Char — та кнопка, по нажатию на которую меню необходимо вызвать.
Так как тип рекурсивный, для него можно просто определить свёртку, чтобы потом, пользуясь ей, запускать меню:
Также нам понадобятся функции, которые более удобно создадут для нас элементы меню. SubMenu создать просто, SubMenu «File» ..., а вот MenuAction пользоваться сложнее. Поэтому определим несколько функций, которые будут принимать действие (такое же, как справа от ?>>! в биндингах). Я приведу код двух из них:
Здесь мы создаём MenuItem, который при нажатии на соответствующую кнопку (char c) закроет меню и вызовет действие, которое нам надо.
И последнее, напишем функцию показа меню.
Полный код можно посмотреть тут.
Теперь стоит создать какое-нибудь меню, забиндить на кнопку и начать можно проверять.
Сначала я написал большое развесистое меню, запихнув туда то, что мне попалось при беглых просмотрах различных модулей в Yi. Когда я заметил, что, например, часто захожу в подменю View — Windows, я решил просто вынести это меню на отдельный хоткей.
Теперь можно сплитить окно не только по длинной комбинации V-W-S, но и просто Ctrl-W — S.
Вот код основного меню и подменю Windows:
Всё меню можно посмотреть тут.
Прописываем главное меню и подменю на различные комбинации.

Пользуемся!

После реализации я прикрутил какой-то встроенный простейший автокомплит, затем интерпретатор GHCi. На очереди тулза hlint (анализирует код и подсказывает, где можно заменить на использование стандартной функции, где написано что-то лишнее и прочее) и прочие.
Весь код доступен на GitHub.
Несколько смущало наличие трёх функций
Исправил на единый action
Из-за отстутствия опыта с 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 item action_ :: (YiAction a x, Show x) => String -> a -> MenuItem action_ title act = action title (const act) -- | Action on item with context action :: (YiAction a x, Show x) => String -> (MenuContext -> a) -> MenuItem action title act = MenuAction title act' where act' ctx c = char c ?>>! (do withEditor closeBufferAndWindowE runAction $ makeAction (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" [ action_ "Quit" askQuitEditor, action "Save" (fwriteBufferE . parentBuffer)], menu "Edit" [ action_ "Auto complete" wordComplete, action_ "Completion" completeWordB], menu "Tools" [ menu "Ghci" ghciMenu], menu "View" [ menu "Windows" windowsMenu, menu "Tabs" tabsMenu, menu "Buffers" buffersMenu, menu "Layout" [ action_ "Next" layoutManagersNextE, action_ "Previous" layoutManagersPreviousE]]] -- | Windows menu windowsMenu :: Menu windowsMenu = [ action_ "Next" nextWinE, action_ "Previous" prevWinE, action_ "Split" splitE, action_ "sWap" swapWinWithFirstE, action_ "Close" tryCloseE, action_ "cLose-all-but-this" closeOtherE]
Всё меню можно посмотреть тут.
Результат
Прописываем главное меню и подменю на различные комбинации.

Пользуемся!

Итоги
После реализации я прикрутил какой-то встроенный простейший автокомплит, затем интерпретатор GHCi. На очереди тулза hlint (анализирует код и подсказывает, где можно заменить на использование стандартной функции, где написано что-то лишнее и прочее) и прочие.
Весь код доступен на GitHub.
Update
Несколько смущало наличие трёх функций
actionB, actionE и actionY. Однако их можно заменить на runAction . makeActionИсправил на единый action
