Меню для Yi

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      +1
      … и, как я понимаю, по возможностям этот редактор сравним с аналогами?
        0
        Задуман он был именно как Vim/Emacs для Haskell.
        По-хорошему он умеет вести себя и как тот, и как другой, однако насколько поставляемые в комплекте «режимы совместимости» хороши, я сказать не могу.
        +3
        Самое сложное в этом редакторе это установить его.

        > cabal update
        > cabal install yi

        cabal: Error: some packages failed to install:
        hint-0.3.3.4 failed during the building phase. The exception was:
        ExitFailure 1
        yi-0.6.5.0 depends on hint-0.3.3.4 which failed to install.

        Попробую ещё через годик другой, а пока vim…
          0
          Windows? Под ним правда собрать проблематично, хотя мне удавалось. В Ubuntu собралось cabal install yi.
          Возможно, я займусь его сборкой под Windows, как руки дойдут.
            0
            Судя по всему, разработку Yi таки забросили. Печально.
              0
              Хотя не совсем. На гуглокоде как-то всё вяло, но на hackage последняя версия всё же 2012-го года.
            • UFO just landed and posted this here
                0
                Многие другие проекты собирались со стёртой базой cabal, cabal update, cabal install , этот — ни разу. Пробовал периодически последние года 2.
                0
                Какой компилятор? hint последний вроде как на 7.4 собирается, а вот на предыдущих — неясно.
                +3
                по названию похоже что Yi это редактор кода для Yii )
                  0
                  Пробовал его использовать, не вышло. Всё же емакс с haskell-mode удобнее кажется. Но это субъективное восприятие, ибо емаксом пользуюсь почти 10 лет, вот и привык.

                  Only users with full accounts can post comments. Log in, please.