tataku.vim というプラグインを作っている話
ここ数か月、tataku.vimというプラグインを作っています。
なぜつくったか
筆者はこれまで、dps-translate-vimやdps-paiza-io-vimなどのdeno(denops.vim)を使ってweb apiを実行するプラグインを作ってきました。
その中で、以下の点が"面倒だな"と感じました。
- プラグインごとにいちいちバッファなどから文字列を取得しなければいけない
- プラグインごとにいちいちバッファに書き出す処理を書かなければいけない
要するにweb apiを叩く部分がメインなのに、それ以外の部分に気を使わないといけないことが気になっていました。
そこで、入力部分と処理部分、出力部分を分けて実装できるような仕組みを作ることにしました。
構成
tataku.vimでは(今のところ)入力部分をcollector
、処理部分をprocessor
、出力部分をemitter
と呼んでいます。
shellのようにcollector | processor[0] | processor[1] ... | emitter
のような流れで文字列を処理していくイメージで作っています。
流れていくデータの型はVimから受け取る文字列を想定しているため、typescriptでいうところのstring[]
になっています。
実際に使うには
tataku.vimはデフォルトでcollector
、processor
、emitter
を含んでいないため、必要なものは別途インストールする必要があります。
(この辺りはddc.vimなどのdark-poweredなプラグインの影響を強く受けています。)
今のところ、以下のものが自分では必要だったので作っています。
- tataku-collector-current_line
- tataku-collector-lorem_ipsum(テスト用)
- tataku-processor-deepl
- tataku-processor-google_translate
- tataku-processor-intl_segmenter
- tataku-emitter-echo
- tataku-emitter-nvim_floatwin
- tataku-emitter-window
tataku.vimではcollector | processor[0] | processor[1] ... | emitter
のような一つの流れをRecipeと呼んでいます。
例えば、現在行のテキストをgoogle翻訳を使って英語から日本語に翻訳し、echoエリアに出力する
というRecipeは以下のような設定を書くことで実現できます。
(vim-plug形式で書いています。ほかのプラグインマネージャの場合は適宜読み替えてください。)
Plug 'vim-denops/denops.vim'
Plug 'Omochice/tataku.vim'
Plug 'Omochice/tataku-collector-current_line'
Plug 'Omochice/tataku-processor-google_translate'
Plug 'Omochice/tataku-emitter-echo'
let g:tataku_recipes = #{
\ translate: #{
\ collector: #{ name: 'current_line', },
\ processor: [#{ name: 'google_translate', options: #{ source: 'en', target: 'ja' }}],
\ collector: #{ name: 'echo' },
\ },
\}
これでtranslate
という名前のRecipeが定義されました。
定義したRecipeを呼び出すためには関数tataku#call_recipe()
を使います。
この関数は引数でRecipeの名前を取ります。
今回の場合、translate
という名前でRecipeを定義しているのでtataku#call_recipe('translate')
を実行することで、日本語に変換された現在行がエコーエリアに出力されます。
operatorを使ったレシピの呼び出し
少し脱線しますが、私はVimと他のエディタを比べた時、Vimの強みになるのはモードという概念とそれを基礎とするoperatorとtextobjectの機能だと考えています。
そこで、tataku.vimをoperatorとして動作させる方法も(暫定的に、ですが)提供しています。
変数g:tataku_enable_operator
をv:true
か1
に設定すると、定義したRecipeをoperatorとして使うことができるようになります。
例えば、先述したtranslate
のRecipeであれば<Plug>(operator-tataku-translate)
でoperatorとして呼び出すことができます。
このとき、Recipe内で定義したcollector
の値は無視され、iw
などのtextobjectを対象としてRecipeを実行します。
processorを自分で定義する方法
collector
やprocessor
、emitter
を定義するには、tataku.vimが定義しているCollector
やProcessor
、Emitter
のインターフェースを満たすクラスを実装する必要があります。
例えば、['aaa', 'bbb', 'ccc']
を受け取った時にそれを['aaaaaa', 'bbbbbb', 'cccccc']
にするようなprocessor
: double
は以下のように書けます。
import { Processor } from "https://raw.githubusercontent.com/Omochice/tataku.vim/master/denops/tataku/interface.ts";
import { Denops } from "https://raw.githubusercontent.com/Omochice/tataku.vim/master/denops/tataku/deps.ts";
import {
isObject,
isString,
} from "https://deno.land/x/unknownutil@v2.0.0/mod.ts";
export default class implements Processor {
constructor(private readonly option: Record<string, unknown>){
if isOption(x) {
throw new Error("option must has 'times' filed")
}
}
async run(_: Denops, source: string[]) {
return source.map((e: string) => e.repeat(this.option.times ?? 2))
}
}
type Option = {
times?: number
}
function isOption(x): x is Option {
return isObject(x) && (isNumber(x.times) || isUndefined(x.times))
}
定義したクラスをVimのruntimepathの中の@tataku/processor/double.ts
でdefault export
することでdouble
という名前でtataku.vimから使えるようになります。
このクラスはrun
という名前の非同期な関数を持つ必要があります。
また、クラスのコンストラクタにはRecipeで設定したoptions
の値が渡されます。
constructor()
やrun()
の中でthrow
した場合、tataku.vimはそれをcatchしてRecipeの実行を止め、ユーザにエラー内容(今回は"option must has 'times' filed"
)を伝えます。
最後に
このプラグインはまだ開発途中なので、もしかするとRecipeの定義方法やoperatorの登録方法等が変わる可能性があります。
(operatorとしての登録が all or nothingになってしまう点やprocessorなどの定義でdefault export
を使わないといけないところなど、個人的にもにょっているところが多々あるので、気が向いたらリファクタする、ぐらいの意味です。)
Discussion