Недавно я всё же решил сесть и разобраться с 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