🐜

denops.vimで括弧補完プラグインを書いた

2021/02/27に公開2

始めに

こんにちは, higashiです.
今回はdenops.vimで括弧補完プラグインを作成したので紹介していきます.

今回作ったプラグイン

dps-kakkonanというプラグインです.

元々作成していたvim-kakkonanというプラグインをdenops.vimを使って実装した形になります.

denops.vimについて

こちらの記事で作者であるlambdalisueさんが解説されています.

簡単に説明するとDenoを使ってVim/Neovimのプラグインを書くことができます.
TypeScriptで記述できるので型を使うことができる他, すでにあるDenoの資産も使えます.

dps-kakkonanの機能

括弧補完です.
普通の括弧補完です.
こちらのgifを見ていただければわかります.
Image from Gyazo

実装について

  • 括弧を補完
  • 右に閉じ括弧がある状態で閉じ括弧を入力すると右に一つカーソルがズレる
  • 開き括弧と閉じ括弧のがカーソルの両隣になる状態でバックスペースを押すと括弧がどちらも削除される

と言った基本の機能しか実装していないためそこまでコードは長くならず, TypeScript 105行で実装できました.

start(async (vim) => {
    const getLineChar = async (diff: number): Promise<string> => {
        if (typeof diff !== "number") {
            throw new Error(`'diff' attribute of 'getLineChar' in must be a number`)
        }

        const cursorStr = await vim.call('getline', '.');
        if (typeof cursorStr !== "string") {
            throw new Error(`'cursorStr' attribute of 'kakkonanCompletion' in must be a string`)
        }

        const cursorLine = await vim.call('line', '.');
        if (typeof cursorLine !== "number") {
            throw new Error(`'cursorLine' attribute of 'kakkonanCompletion' in must be a number`)
        }

        const cursorCol = await vim.call('col', '.');
        if (typeof cursorCol !== "number") {
            throw new Error(`'cursorCol' attribute of 'kakkonanCompletion' in must be a number`)
        }

        const cursorChar = cursorStr.substr(cursorCol + diff, 1);

        return cursorChar;
    }
})

このようにアロー関数で関数を登録できたのはとても書きやすくて良かったです.

また, 返り値がVim/Neovimにそのまま渡されるのでこのような関数をinoremap等でinsert modeから呼び出すと返り値がファイルに挿入されます.

async hogehogefugafuga(inputBrackets: unknown): Promise<string> {
    if (typeof inputBrackets !== "string") {
        throw new Error(`'inputBrackets' attribute of 'kakkonanCompletion' in must be a string`);
    }

    return "hogehoge" + inputBrackets;
}

この場合だとhogehogeに引数の文字列が結合されて挿入されます.
この仕様を活かして括弧補完を作成しました.

キーマップの定義も簡単で, vim.execute()の中に普段vimrcに書いているようなキーマップの設定を入れるだけです.

vim.execute(`
    inoremap <expr> ( denops#request("kakkonan", "kakkonanCompletion", ['(']) . "\<left>"
    inoremap <expr> { denops#request("kakkonan", "kakkonanCompletion", ['{']) . "\<left>"
    inoremap <expr> [ denops#request("kakkonan", "kakkonanCompletion", ['[']) . "\<left>"
    inoremap <expr> " denops#request("kakkonan", "kakkonanCompletion", ['"']) != "" ? '""' . "\<left>" : "\<right>"
    inoremap <expr> ' denops#request("kakkonan", "kakkonanCompletion", ["'"]) != "" ? "''" . "\<left>" : "\<right>"
    inoremap <expr> ${backQuote} denops#request("kakkonan", "kakkonanCompletion", ['${backQuote}']) != "" ? '${backQuote + backQuote}' . "\<left>" : "\<right>"
    inoremap <expr> ) denops#request("kakkonan", "kakkonanEscapeBrackets", [')']) == v:false ? ")" : "\<right>"
    inoremap <expr> } denops#request("kakkonan", "kakkonanEscapeBrackets", ['}']) == v:false ? "}" : "\<right>"
    inoremap <expr> ] denops#request("kakkonan", "kakkonanEscapeBrackets", [']']) == v:false ? "]" : "\<right>"
    inoremap <expr> <CR> denops#request("kakkonan", "kakkonanBackSpaceEnter", []) == v:false ? "\<CR>" : "\<CR>\<C-o>\<S-o>"
    inoremap <expr> <BS> denops#request("kakkonan", "kakkonanBackSpaceEnter", []) == v:false ? "\<BS>" : "\<BS>\<right>\<BS>"
`);

これはdps-kakkonan内で定義しているコマンドです.
このように複数行文字列を渡すことで一気にコマンドの定義が行えます.

ちなみに以下のように一つづつ定義することも可能です.

vim.execute(`inoremap <expr> ( denps#request("kakkonan", "kakkonanCompletion", ['('])`)
vim.execute(`inoremap <expr> [ denps#request("kakkonan", "kakkonanCompletion", ['['])`)

最後に

denops.vimのおかげでDenoを使ってVim plugin作成ができるようになりました.
このようにプラグイン作成の幅が広がっていくのはとても良いと思います.

これを機にTypeScriptもしっかり勉強したいです.

ここまで読んでくれた皆様ありがとうございました.
denops.vimに興味を持った方はぜひvim-jpのSlack内にある#tech-denopsにも参加してみてください.

vim-jp Slackへの参加はこちらから

Discussion

AlisueAlisue

少し気になったのでアドバイスです

getLineChar() ですが

  1. number 型で受け取っている diff の型チェックをする必要はない
  2. Vim が返す値は使い方を間違えていない限り型はわかるので as で書いてしまっても良い
    (これは好みの部分も大きいですが、僕はユーザーが関与しない部分はプログラムを書いている側で責任を取れるので as で簡易化しても良いという考えです)

これを踏まえると、以下のように簡略化できます。

const getLineChar = async (diff: number): Promise<string> => {
  const cursorStr = await vim.call("getline", ".") as string;
  const cursorLine = await vim.call("line", ".") as number;
  const cursorCol = await vim.call("col", ".") as number;
  const cursorChar = cursorStr.substr(cursorCol + diff, 1);
  return cursorChar;
};
higashihigashi

ありがとうございます!
確かにユーザーが関与しない部分はasで簡略化しても良いかもしれませんね