Меню для Yi

в 7:07, , рубрики: haskell, метки:

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

image

Настраиваем простые хоткеи

Для начала следует разобраться, как же устроен 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]

Всё меню можно посмотреть тут.

Результат

Прописываем главное меню и подменю на различные комбинации.
Меню для Yi
Пользуемся!
Меню для Yi

Итоги

После реализации я прикрутил какой-то встроенный простейший автокомплит, затем интерпретатор GHCi. На очереди тулза hlint (анализирует код и подсказывает, где можно заменить на использование стандартной функции, где написано что-то лишнее и прочее) и прочие.

Весь код доступен на GitHub.

Автор: VoidEx

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js