Vimgolf client

You might have heard that Yi being extensible in Haskell offers the ability to use existing Haskell libraries. In this page, I’ll demonstrate the extensibility of Yi by writing a Vimgolf client. The client is very rudimentary, being able to get the required output, and the starting text. From there, the user can modify the starting text, and if the user succeeds, the buffer text is changed to success, else, the buffer text is changed to failure.

First, we need to get JSON data from vimgolf. This typically has the format "http://vimgolf.com/challenges/" ++ challenge-id ++ ".json". We can use http-conduit to fetch this using http. Let’s start with just this much. Create a file ~/.config/yi/lib/Vimgolf.hs and put the following in the file:

{-# LANGUAGE OverloadedStrings #-}
module Vimgolf where

import qualified Data.ByteString.Lazy.Char8 as BS (ByteString(..))
import           Network.HTTP.Conduit             (simpleHttp)

getJSON :: String -> IO BS.ByteString
getJSON challenge = simpleHttp ("http://vimgolf.com/challenges/" ++ challenge ++ ".json")

Now that we have a way of getting the json data, let’s convert that to haskell’s record syntax so that it would become useful. You can have a look at vimgolf’s json data to make sense out of this. An example is here. You would need to add the following to convert to Haskell’s record syntax. This should be familiar to you if you are used to the Aeson library for JSON parsing.

import           Control.Applicative              ((<$>), (<*>))
import           Data.Aeson                       ((.:), FromJSON(..), eitherDecode, Value(..))

data VGTopLevel = VGTopLevel { getInput  :: VGData
                             , getOutput :: VGData
                             , vimRC     :: String
                             , client    :: String
                             } deriving (Show)

data VGData = VGData { getData :: String
                     , getType :: String
                     } deriving (Show)

instance FromJSON VGTopLevel where
    parseJSON (Object v) = VGTopLevel <$> v .: "in"
                                      <*> v .: "out"
                                      <*> v .: "vimrc"
                                      <*> v .: "client"

instance FromJSON VGData where
    parseJSON (Object v) = VGData <$> v .: "data"
                                  <*> v .: "type"

getVimgolfJSON :: String -> IO VGTopLevel
getVimgolfJSON str = do
    d <- (eitherDecode <$> getJSON str) :: IO (Either String VGTopLevel)
    case d of
        Left err -> error "FAIL"
        Right vg -> return vg

And lastly, you would need make this accessible in Yi. This can be done by adding the following:

import           Yi
import qualified Yi.Rope as R (toString, fromString)
import           Yi.Utils     (io)

import           Control.Lens                     (assign)
import           Data.Text                        (Text(..))
import qualified Data.ByteString.Lazy.Char8 as BS (ByteString(..))

checkChallenge :: String -> YiM ()
checkChallenge challenge = do
    vg <- io $ getVimgolfJSON challenge
    withCurrentBuffer $ do buf <- readRegionB =<< regionOfB Document
                           if ((R.toString buf) == ((getData . getOutput) vg))
                           then replaceBufferContent "SUCCESS"
                           else replaceBufferContent "FAILURE"

getChallenge :: String -> YiM ()
getChallenge challenge = do
    vg <- io $ getVimgolfJSON challenge
    strFun "OUTPUT"
    withCurrentBuffer $ insertN (R.fromString ((getData . getOutput) vg))
    withEditor splitE
    bufRef <- withEditor newTempBufferE
    strFun "INPUT"
    withCurrentBuffer $ insertN (R.fromString ((getData . getInput) vg))
    where strFun = withCurrentBuffer . assign identA . MemBuffer

Putting all of this together, you would get this final file:

{-# LANGUAGE OverloadedStrings #-}

module Vimgolf where

import           Yi
import qualified Yi.Rope as R (toString, fromString)
import           Yi.Utils     (io)

import           Control.Applicative              ((<$>), (<*>))
import           Control.Lens                     (assign)
import           Data.Aeson                       ((.:), FromJSON(..), eitherDecode, Value(..))
import           Data.Text                        (Text(..))
import qualified Data.ByteString.Lazy.Char8 as BS (ByteString(..))
import           Network.HTTP.Conduit             (simpleHttp)

getJSON :: String -> IO BS.ByteString
getJSON challenge = simpleHttp ("http://vimgolf.com/challenges/" ++ challenge ++ ".json")

data VGTopLevel = VGTopLevel { getInput  :: VGData
                             , getOutput :: VGData
                             , vimRC     :: String
                             , client    :: String
                             } deriving (Show)

data VGData = VGData { getData :: String
                     , getType :: String
                     } deriving (Show)

instance FromJSON VGTopLevel where
    parseJSON (Object v) = VGTopLevel <$> v .: "in"
                                      <*> v .: "out"
                                      <*> v .: "vimrc"
                                      <*> v .: "client"

instance FromJSON VGData where
    parseJSON (Object v) = VGData <$> v .: "data"
                                  <*> v .: "type"

getVimgolfJSON :: String -> IO VGTopLevel
getVimgolfJSON str = do
    d <- (eitherDecode <$> getJSON str) :: IO (Either String VGTopLevel)
    case d of
        Left err -> error "FAIL"
        Right vg -> return vg

checkChallenge :: String -> YiM ()
checkChallenge challenge = do
    vg <- io $ getVimgolfJSON challenge
    withCurrentBuffer $ do buf <- readRegionB =<< regionOfB Document
                           if ((R.toString buf) == ((getData . getOutput) vg))
                           then replaceBufferContent "SUCCESS"
                           else replaceBufferContent "FAILURE"

getChallenge :: String -> YiM ()
getChallenge challenge = do
    vg <- io $ getVimgolfJSON challenge
    renameBuffer "OUTPUT"
    withCurrentBuffer $ insertN (R.fromString ((getData . getOutput) vg))
    withEditor splitE
    bufRef <- withEditor newTempBufferE
    renameBuffer "INPUT"
    withCurrentBuffer $ insertN (R.fromString ((getData . getInput) vg))
    where renameBuffer = withCurrentBuffer . assign identA . MemBuffer

And finally, we need a way to call this from Yi. This can be done with a vim config as follows:

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK show-extensions #-}

module Main (main) where

import           Yi hiding
import qualified Yi.Keymap.Vim        as V2
import qualified Yi.Keymap.Vim.Common as V2
import qualified Yi.Keymap.Vim.Utils  as V2

import           Data.Monoid (mappend)
import           Vimgolf (getChallenge, checkChallenge)

main = yi $ myConfig

myConfig :: Config
myConfig = defaultVimConfig { defaultKm = v2KeymapSet $ myBindings }

myBindings :: (V2.EventString -> EditorM ()) -> [V2.VimBinding]
myBindings eval =
       [ nmap' (leader "s") (getChallenge vgChallenge)
       , nmap' (leader "e") (checkChallenge vgChallenge)
       ]

vgChallenge :: String
vgChallenge = "540629666a1e4000020d9e5a"

v2KeymapSet :: ((V2.EventString -> EditorM ()) -> [V2.VimBinding]) -> KeymapSet
v2KeymapSet myBindings = V2.mkKeymapSet $ V2.defVimConfig `override` \super this ->
    let eval = V2.pureEval this
    in super {
          V2.vimBindings = myBindings eval ++ V2.vimBindings super
        }

nmap'  x y = V2.mkStringBindingY V2.Normal (x, y, id)

leader :: V2.EventString -> V2.EventString
leader = mappend "\\"

The challenge id is set in the config file. You can always edit the config file and use the yi function reload to restart as :yi reload in vim.

Now, you can open up yi, and with an empty buffer, press \s to get the vimgolf challenge, edit the bottom buffer to match the top buffer, and press \e to check your result. We could easily extend this example to use the minibuffer, have a retry option, etc.