Vimで日本語編集時の f, t の移動や操作を楽にするプラグイン ftjpn を作りました

ftjpn 日本語編集時のft操作を便利にするプラグイン

日本語でftTF(以後f)を使って移動、削除、ヤンクするとき、fを押してから一旦IMEをONにしないと使えません。めんどくさい……。

これでは誰も日本語でfなんて使いませんよね。

日本語編集をやりやすくする方法としては、インサートモードでの横移動のキーバインドを設定したり、)(WBEの移動を使いやすくするプラグインの導入などがあります。人によってはこれらで十分かもしれません。

しかし、それでもやっぱりせっかく用意されているデフォルトのfを有効活用しないのはもったいないと思い、Vim scriptの勉強も兼ねてこのプラグインを作成しました(つまりクオリティは保証しません)。

出来るようになること

..に対応

.vimrcでキーリストを以下のように設定

let g:ftjpn_key_list = [
    \ ['.', '。']
    \ ]

配列の先頭の文字がキーです。上のように設定すれば、..を担当するようになります。

こんには世界。ハローワールドですね!

FtT もそれぞれキーに対応した動作をします。

もちろん.の機能は失いません。Hello World.のような行で使えば ..として認識されます。

うーん、便利。

オプション設定

さらにキーを増やす

let g:ftjpn_key_list = [
    \ ['.', '。', '.']
    \ [',', '、', ',']
    \ ['g', 'が']
    \ ['w', 'を']
    \ ['(', '(', ')']
    \ [';', '!', '?']
    \ ]

このように設定を増やしていけば、半角の,で全角のgに対して操作出来るようになります。

キーは1対1のペアである必要はなく、;を担わせることも可能です。ただし、あまり増やしすぎると邪魔になることもあります。

これで、今まで対象の文字が全角であるがゆえに躊躇していたコマンドを惜しげもなく連発出来るようになるでしょう。

fそのものを変えたい場合

fを独自に設定したい場合は.vimrcに以下のように記述します。

f<leader>fにマッピング

let g:ftjpn_no_defalut_key_mappings = 1

let g:ftjpn_f = '<leader>f'
let g:ftjpn_F = '<leader>F'
let g:ftjpn_t = '<leader>t'
let g:ftjpn_T = '<leader>T'

半角同士でもOK

let g:ftjpn_key_list = [
    \ [';', '^', '$', '*', '#', '~', '%']
    \ ]

記号を打つのはシフトキーを押さなくてはいけないのでめんどくさいです。上のように設定すれば;キーだけで複数の記号に対応できます。

ただし、狙った場所に1発で到着できなくなるケースが出てきます。

var str = '${str1}文字列結合${str2}'; // Template literal の活用

上の例で行頭から;に飛ぼうとすると、$で引っかかってしまいます。3f;と打っても駄目です。最寄りが$な時点で数えるのは$だけになり、この例の場合2f;までしか受け付けてくれません。

このようなことがあるので闇雲に対応する文字を増やすのは考えものです。多くの場合;は行末で使われるので移動する上であまり影響はないかも知れませんが。

dfcfのことも考えると更にややこしくなってしまうので、半角同士のグループは設定しないというのも手だと思います。

問題点。英語と日本が混ざった時

こんにちは世界。I am Japanese.

このようなとき、行頭で f.を実行するとこんにちは世界。に飛びます。

2f.としてもI am Japanese..に飛ぶとは出来ません。悲しいです。

解決策は2つ。1つはf. f.と2回連続して打って移動。もう1つはfを押してから1秒待って.を押すことです(待機時間終了後に実行する)。そうすればプラグインとしての機能ではなく、デフォルトのfとして動作するのでが無視されます。

このようなケースはほとんどないとはいえ、ちょっと苦しいので改善策を模索中です。

このプラグインがやっていること

.で近いほうを選ぶ

" 一部の記号を検索用に正規表現の形にする関数
function! s:ConvertRegex(char) abort
    if a:char ==# '.'
        return '\.'
    elseif a:char ==# '*'
        return '\*'
    elseif a:char ==# '^'
        return '\^'
    elseif a:char ==# '$'
        return '\$'
    elseif a:char ==# '['
        return '\['
    elseif a:char ==# '~'
        return '\~'
    else
        return a:char
    endif
endfunction

" f, t 前方検索で利用する char の選定
function! s:SetForwardChar(pattern) abort

    " 現在のカーソル位置から行末までの文字列を取得する
    let col = col('.') - 1
    let line = getline('.')[col:]
    " 文字の位置を比較するときに使う辞書
    let dict = {}

    " 受け取った配列から1つずつ文字を取り出し、現在行の中にあるかチェック
    for char in a:pattern
        " 一部の文字は正規表現の形にする必要がある
        let keyword = s:ConvertRegex(char)
        let matchcol = match(line, keyword, 1, 1)
        " 文字(key)と位置(value)の辞書に値をセット
        if matchcol > 0
            let dict[char] = matchcol
        endif
    endfor 

    if len(dict) == 0
        " 何も見つからなかったら何もしない
        return ''
    else
        " 最も近い位置のcolを数値で取得
        let min_col = min(dict)
        for [key, value] in items(dict)
            if value ==# min_col
                " 最も近いcolの値(min_col)と辞書の値(value)が一致した場合
                " keyを正規表現から普通の形に戻して返す
                return s:RevertRegex(key)
            endif
        endfor
    endif
endfunction

function! ftjpn#Jfmove(pattern) abort
    exe "silent normal! " . v:count1 . "f" . s:SetForwardChar(a:pattern)
endfunction

nnoremap <silent> f. :<C-u>call ftjpn#Jfmove('['.', '']')<CR>

文字を取得するまでは getline() や col()、match() などを使っていますが、最終的に実行されるコマンドはデフォルトのものです。

f.と打ったらカーソルより前方の文字の中から.と。を検索開始。

先にが見つかったらf。を実行。

先に.が見つかったらf.を実行。

たったこれだけです。

ドットリピートの対応

最終的に実行されるのがデフォルトのコマンドならドットリピートも自然と出来るだろうと思っていたのですが無理でした。

f;,で繰り返し動作するので問題ないのですが、dfcfのリピートが上手くいかないのです。

function! s:hoge()
    if 「.」の場合
        execute "normal! f."
    else if 「。」の場合
        execute "normal! f。"
endfunction
onoremap f. :<C-u>call <SID>hoge()

上のように記述するとdf.cf.をドットリピート出来ません。ノーマルコマンドを実行しているだけなのになぜ?と頭を悩ませていたのですが、<expr>を使うことでこの問題を解決できました。

<expr>でドットリピートが可能になる

function! s:hoge()
    if 「.」の場合
        return "f."
    else if 「。」の場合
        return "f。"
endfunction
onoremap <expr> f. <SID>hoge()

複雑なことは出来ないようなのですが、デフォルト機能をちょっと拡張したいときに便利です。

  1. 参考: incsearch.vimでVimの検索体験をリッチにする
  2. 参考: help: map-<expr>

まとめ。普通のVimを逸脱せずに日本語対応できた、と思う。確信はない

これで新たにキーマッピングを増やすことなく全角文字に対応する環境を用意できたはずです。

もっと便利にする方法はあると思いますが、あくまでもデフォルトに寄り添った形で実現したかったのでこれでよしとします。

諸々何かしら穴があるだろうとは思いますが、とりあえず期待通りのものは完成しました。

使いつつ改善していこうと思います。

ftjpn 日本語編集時のft操作を便利にするプラグイン

今回のプラグイン作成で、getline()やcol()、match()、search()のような定番の関数の使い方や引数の並び、意味を少し理解できました。

このあたりの関数の意味がわかるとヘルプを読むのも苦痛でなくなり、むしろ楽しくなってきます。

やはり一度何か作ってみるものだなと、月並みな感想で締めくくりたいと思います。

所感 ~補足の与太話~

Vimで日本語を編集する際、ftがうまく使えないのでなんとか楽にしたいと思っていました。いちいちIMEをONとOFFを切り替えるのは辛すぎます。

結局、wを連発して行き過ぎたらhで戻る、みたいなちまちまとしたカーソル移動を繰り返すことで対処していました。

インサートモードの長期滞在を前提にコントロールキーを使って移動するキーバインドを設定してみたり、f移動のためにを新たにキーマッピングする(など)といった方法を試した時期もあります。

しかし、手数が増える方法はめんどくさいので結局使わなくなります(なりました。コントロールやシフトを使ったらそれはもはや一手じゃなく二手だと思ってしまう派です)。

そして最終的に「そもそも英語のときと同じように操作出来るのが一番いいんじゃないか?」という結論に至りました。

同じ Vim を使っているのに、英語でプログラムを書くときと日本語の文章を書くときで操作が大きく異るのはなんか違う気がしたからです。

それに高速でテキストを編集する凄腕 Vimmer になるためには、日本語を使っているときも Vim ならではの操作を多用して修行するのが王道だと思います。きっと。

日本語文書の編集時でもノーマルモード滞在が快適なのは非常にいいもんです。